diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 68e5096..3b51ec7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,9 +10,39 @@ "WebFetch(domain:developers.cloudflare.com)", "Bash(git add:*)", "Bash(mkdir:*)", - "Bash(cp:*)" + "Bash(cp:*)", + "Bash(npm run init-db:*)", + "Bash(npm start)", + "Bash(set PORT=3001)", + "Bash(node:*)", + "Bash(curl:*)", + "Bash(mv:*)", + "Bash(true)", + "Bash(PORT=3001 npm start)", + "Bash(sqlite3:*)", + "Bash(PORT=3002 npm start)", + "Bash(taskkill:*)", + "Bash(rm:*)", + "mcp__playwright__browser_navigate", + "mcp__playwright__browser_type", + "mcp__playwright__browser_click", + "mcp__playwright__browser_snapshot", + "mcp__playwright__browser_handle_dialog", + "mcp__playwright__browser_close", + "mcp__playwright__browser_console_messages", + "mcp__playwright__browser_press_key", + "mcp__playwright__browser_file_upload", + "mcp__playwright__browser_select_option", + "Bash(tasklist)", + "Bash(start http://localhost:8000)", + "Bash(npm --version)", + "mcp__sequential-thinking__sequentialthinking" ], "deny": [], - "ask": [] - } + "ask": [], + "additionalDirectories": [ + "C:\\c\\Users\\COMTREE\\claude_code" + ] + }, + "default-mode": "plan" } \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2c1347 --- /dev/null +++ b/.gitignore @@ -0,0 +1,151 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# Nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Upload files (production) +uploads/* +!uploads/.gitkeep + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary files +*.tmp +*.temp diff --git a/.playwright-mcp/-.hwp b/.playwright-mcp/-.hwp new file mode 100644 index 0000000..c3e18e2 Binary files /dev/null and b/.playwright-mcp/-.hwp differ diff --git a/.playwright-mcp/-.zip b/.playwright-mcp/-.zip new file mode 100644 index 0000000..4895e1e Binary files /dev/null and b/.playwright-mcp/-.zip differ diff --git a/.playwright-mcp/-2025-7-xls-filename-UTF-8-EC-84-B8-EB-B6-80-20-EC-A7-80-EA-B8-89-EB-82-B4-EC-97-AD-282025-7-EC-9B-94-20-EC-8B-AC-EC-95-BC-20-EB-B9-84-EC-83-81-EA-B7-BC-EB-AC-B4-29-EC-8B-A0-EB-8F-99-ED-9D-AC.xls b/.playwright-mcp/-2025-7-xls-filename-UTF-8-EC-84-B8-EB-B6-80-20-EC-A7-80-EA-B8-89-EB-82-B4-EC-97-AD-282025-7-EC-9B-94-20-EC-8B-AC-EC-95-BC-20-EB-B9-84-EC-83-81-EA-B7-BC-EB-AC-B4-29-EC-8B-A0-EB-8F-99-ED-9D-AC.xls new file mode 100644 index 0000000..1a20d92 Binary files /dev/null and b/.playwright-mcp/-2025-7-xls-filename-UTF-8-EC-84-B8-EB-B6-80-20-EC-A7-80-EA-B8-89-EB-82-B4-EC-97-AD-282025-7-EC-9B-94-20-EC-8B-AC-EC-95-BC-20-EB-B9-84-EC-83-81-EA-B7-BC-EB-AC-B4-29-EC-8B-A0-EB-8F-99-ED-9D-AC.xls differ diff --git a/.playwright-mcp/-6-.png b/.playwright-mcp/-6-.png new file mode 100644 index 0000000..f229d64 Binary files /dev/null and b/.playwright-mcp/-6-.png differ diff --git a/.playwright-mcp/-hwp-filename-UTF-8-EB-AC-B8-ED-99-94-EC-B2-B4-ED-97-98-EA-B4-80-20-EA-B8-B0-EA-B0-84-EC-A0-9C-EA-B7-BC-EB-A1-9C-EC-9E-90-20-EC-B1-84-EC-9A-A9-20-EA-B3-B5-EA-B3-A0-EB-AC-B8.hwp b/.playwright-mcp/-hwp-filename-UTF-8-EB-AC-B8-ED-99-94-EC-B2-B4-ED-97-98-EA-B4-80-20-EA-B8-B0-EA-B0-84-EC-A0-9C-EA-B7-BC-EB-A1-9C-EC-9E-90-20-EC-B1-84-EC-9A-A9-20-EA-B3-B5-EA-B3-A0-EB-AC-B8.hwp new file mode 100644 index 0000000..c3e18e2 Binary files /dev/null and b/.playwright-mcp/-hwp-filename-UTF-8-EB-AC-B8-ED-99-94-EC-B2-B4-ED-97-98-EA-B4-80-20-EA-B8-B0-EA-B0-84-EC-A0-9C-EA-B7-BC-EB-A1-9C-EC-9E-90-20-EC-B1-84-EC-9A-A9-20-EA-B3-B5-EA-B3-A0-EB-AC-B8.hwp differ diff --git a/.playwright-mcp/20250807-084242.jpg b/.playwright-mcp/20250807-084242.jpg new file mode 100644 index 0000000..92add3f Binary files /dev/null and b/.playwright-mcp/20250807-084242.jpg differ diff --git a/.playwright-mcp/문화체험관-다도체험-운영.zip b/.playwright-mcp/문화체험관-다도체험-운영.zip new file mode 100644 index 0000000..afb6cbb Binary files /dev/null and b/.playwright-mcp/문화체험관-다도체험-운영.zip differ diff --git a/admin/api-client.js b/admin/api-client.js new file mode 100644 index 0000000..f0a7dcb --- /dev/null +++ b/admin/api-client.js @@ -0,0 +1,260 @@ +// 관리자용 API 클라이언트 +// SQLite 백엔드와 통신하는 함수들 + +const API_BASE_URL = ''; + +// API 요청 헬퍼 함수 +async function apiRequest(url, options = {}) { + const fullUrl = `${API_BASE_URL}${url}`; + console.log('🌐 API 요청:', options.method || 'GET', fullUrl); + console.log('요청 옵션:', options); + + const response = await fetch(fullUrl, { + credentials: 'include', // 세션 쿠키 포함 + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }); + + console.log('📨 응답 받음:', response.status, response.statusText); + console.log('응답 URL:', response.url); + + if (!response.ok) { + const error = await response.text(); + console.error('❌ API 오류 응답:', error); + throw new Error(`API Error: ${response.status} - ${error}`); + } + + return response; +} + +// 인증 관련 API +const AuthAPI = { + // 현재 세션 확인 + async getSession() { + try { + const response = await apiRequest('/api/auth/session'); + return await response.json(); + } catch (error) { + console.error('세션 확인 오류:', error); + return { user: null }; + } + }, + + // 로그인 + async login(email, password) { + try { + const response = await apiRequest('/api/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }) + }); + return await response.json(); + } catch (error) { + console.error('로그인 오류:', error); + throw error; + } + }, + + // 로그아웃 + async logout() { + try { + await apiRequest('/api/auth/logout', { + method: 'POST' + }); + } catch (error) { + console.error('로그아웃 오류:', error); + throw error; + } + } +}; + +// 파일 관리 API +const FilesAPI = { + // 모든 파일 조회 (관리자용) + async getAll() { + try { + const response = await apiRequest('/api/files'); + return await response.json(); + } catch (error) { + console.error('파일 목록 조회 오류:', error); + throw error; + } + }, + + // 공개 파일 조회 (일반 사용자용) + async getPublic() { + try { + const response = await apiRequest('/api/files/public'); + return await response.json(); + } catch (error) { + console.error('공개 파일 목록 조회 오류:', error); + throw error; + } + }, + + // 파일 추가 + async create(formData) { + try { + const response = await fetch('/api/files', { + method: 'POST', + credentials: 'include', + body: formData // FormData는 Content-Type 헤더를 자동 설정 + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`API Error: ${response.status} - ${error}`); + } + + return await response.json(); + } catch (error) { + console.error('파일 추가 오류:', error); + throw error; + } + }, + + // 파일 수정 (FormData 지원) + async update(id, data) { + try { + let requestOptions; + + if (data instanceof FormData) { + // FormData인 경우 (파일 업로드 포함) + requestOptions = { + method: 'PUT', + credentials: 'include', + body: data // FormData는 Content-Type 헤더를 자동 설정 + }; + + console.log('📁 FormData를 사용한 파일 수정 요청'); + const response = await fetch(`/api/files/${id}`, requestOptions); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`API Error: ${response.status} - ${error}`); + } + + return await response.json(); + } else { + // 일반 JSON 데이터인 경우 + const response = await apiRequest(`/api/files/${id}`, { + method: 'PUT', + body: JSON.stringify(data) + }); + return await response.json(); + } + } catch (error) { + console.error('파일 수정 오류:', error); + throw error; + } + }, + + // 파일 삭제 + async delete(id) { + try { + await apiRequest(`/api/files/${id}`, { + method: 'DELETE' + }); + } catch (error) { + console.error('파일 삭제 오류:', error); + throw error; + } + }, + + // 파일 다운로드 + async download(fileId, attachmentId) { + try { + const response = await apiRequest(`/api/download/${fileId}/${attachmentId}`); + return response; + } catch (error) { + console.error('파일 다운로드 오류:', error); + throw error; + } + } +}; + +// 카테고리 관리 API +const CategoriesAPI = { + // 모든 카테고리 조회 + async getAll() { + try { + const response = await apiRequest('/api/categories'); + return await response.json(); + } catch (error) { + console.error('카테고리 목록 조회 오류:', error); + throw error; + } + }, + + // 카테고리 추가 + async create(name) { + try { + const response = await apiRequest('/api/categories', { + method: 'POST', + body: JSON.stringify({ name }) + }); + return await response.json(); + } catch (error) { + console.error('카테고리 추가 오류:', error); + throw error; + } + }, + + // 카테고리 수정 + async update(id, name) { + try { + const url = `/api/categories/${id}`; + console.log('🔄 카테고리 수정 API 호출:', url); + console.log('전송 데이터:', { id, name }); + + const response = await apiRequest(url, { + method: 'PUT', + body: JSON.stringify({ name }) + }); + + console.log('API 응답 상태:', response.status); + const result = await response.json(); + console.log('API 응답 데이터:', result); + return result; + } catch (error) { + console.error('카테고리 수정 오류:', error); + throw error; + } + }, + + // 카테고리 삭제 + async delete(id) { + try { + await apiRequest(`/api/categories/${id}`, { + method: 'DELETE' + }); + } catch (error) { + console.error('카테고리 삭제 오류:', error); + throw error; + } + } +}; + +// 연결 테스트 API +const SystemAPI = { + // 서버 연결 테스트 + async testConnection() { + try { + const response = await fetch('/api/health'); + return response.ok; + } catch (error) { + console.error('연결 테스트 오류:', error); + return false; + } + } +}; + +// 전역으로 내보내기 +window.AdminAPI = { + Auth: AuthAPI, + Files: FilesAPI, + Categories: CategoriesAPI, + System: SystemAPI +}; diff --git a/admin/index.html b/admin/index.html index 6f173a4..7ec02c7 100644 --- a/admin/index.html +++ b/admin/index.html @@ -3,45 +3,72 @@ - 자료실 - CRUD 시스템 + 자료실 - 관리자 -
-

📚 자료실 관리 시스템

-

파일과 문서를 효율적으로 관리하세요

-
-
-
- - - -
+ + - + + + + + + \ No newline at end of file diff --git a/admin/script.js b/admin/script.js index f2ecb83..44825d2 100644 --- a/admin/script.js +++ b/admin/script.js @@ -1,923 +1,1149 @@ -class FileManager { +class AdminFileManager { constructor() { this.files = []; - this.allFiles = []; // 전체 파일 목록 - this.currentPage = 1; // 현재 페이지 - this.selectedFiles = []; // 선택된 파일들 + this.categories = []; + this.currentPage = 1; + this.itemsPerPage = 10; + this.filteredFiles = []; this.currentEditId = null; + this.currentEditCategoryId = null; this.currentUser = null; - this.isOnline = navigator.onLine; - this.realtimeSubscription = null; - this.authMode = 'login'; // 'login' or 'signup' - this.isOfflineMode = true; // 강제 오프라인 모드 + this.isLoggedIn = false; this.init(); } async init() { - console.log('🔍 FileManager 초기화 시작'); + console.log('🔍 Admin FileManager 초기화 시작'); - // Supabase 초기화 - 임시로 false로 설정 (Storage 오류 우회) - const supabaseInitialized = false; // initializeSupabase(); - console.log('🔍 supabaseInitialized:', supabaseInitialized); - - if (supabaseInitialized) { - console.log('✅ Supabase 모드로 실행합니다.'); - await this.initializeAuth(); - } else { - console.log('⚠️ 오프라인 모드로 실행합니다.'); - // 오프라인 모드에서는 가상 사용자 설정 - this.currentUser = { id: 'offline-user', email: 'offline@local.com' }; - console.log('🔍 가상 사용자 설정:', this.currentUser); - - this.files = this.loadFiles(); - console.log('🔍 파일 로드 완료:', this.files.length, '개'); - - this.showOfflineMode(); - this.updateAuthUI(); - console.log('🔍 UI 업데이트 완료'); + try { + this.bindEvents(); + await this.checkSession(); + this.updateUI(); + } catch (error) { + console.error('초기화 오류:', error); + this.showNotification('초기화 중 오류가 발생했습니다.', 'error'); } - - this.bindEvents(); - this.renderFiles(); - this.updateEmptyState(); - this.setupOnlineStatusListener(); - - // 인증 함수들을 빈 함수로 덮어씌움 (완전 차단) - this.overrideAuthFunctions(); } bindEvents() { - // 기존 이벤트 - document.getElementById('fileForm').addEventListener('submit', (e) => this.handleSubmit(e)); - document.getElementById('cancelBtn').addEventListener('click', () => this.clearForm()); - document.getElementById('searchBtn').addEventListener('click', () => this.handleSearch()); - document.getElementById('searchInput').addEventListener('keyup', (e) => { - if (e.key === 'Enter') this.handleSearch(); - }); - document.getElementById('categoryFilter').addEventListener('change', () => this.handleSearch()); - document.getElementById('sortBy').addEventListener('change', () => this.renderFiles()); - document.getElementById('editForm').addEventListener('submit', (e) => this.handleEditSubmit(e)); - document.getElementById('closeModal').addEventListener('click', () => this.closeEditModal()); - document.getElementById('fileUpload').addEventListener('change', (e) => this.handleFileUpload(e)); + // 로그인 이벤트 + const loginBtn = document.getElementById('loginBtn'); + const adminPassword = document.getElementById('adminPassword'); + + if (loginBtn) { + loginBtn.addEventListener('click', () => this.handleLogin()); + } + + if (adminPassword) { + adminPassword.addEventListener('keyup', (e) => { + if (e.key === 'Enter') this.handleLogin(); + }); + } + + // 로그아웃 이벤트 + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn) { + logoutBtn.addEventListener('click', () => this.handleLogout()); + } + + // 검색 및 정렬 이벤트 + const searchBtn = document.getElementById('searchBtn'); + const searchInput = document.getElementById('searchInput'); + const categoryFilter = document.getElementById('categoryFilter'); + const sortBy = document.getElementById('sortBy'); + + if (searchBtn) searchBtn.addEventListener('click', () => this.handleSearch()); + if (searchInput) { + searchInput.addEventListener('keyup', (e) => { + if (e.key === 'Enter') this.handleSearch(); + }); + } + if (categoryFilter) categoryFilter.addEventListener('change', () => this.handleSearch()); + if (sortBy) sortBy.addEventListener('change', () => this.handleSearch()); + + // 탭 전환 이벤트 + this.bindTabEvents(); + + // 파일 관리 이벤트 + this.bindFileEvents(); + + // 카테고리 관리 이벤트 + this.bindCategoryEvents(); // 페이지네이션 이벤트 - document.getElementById('prevPage').addEventListener('click', () => this.goToPrevPage()); - document.getElementById('nextPage').addEventListener('click', () => this.goToNextPage()); - - // 드래그 앤 드롭 이벤트 - this.setupDragAndDrop(); + this.bindPaginationEvents(); + } - // 인증 이벤트 (오프라인 모드에서는 비활성화) - if (window.supabase) { - document.getElementById('loginBtn').addEventListener('click', () => this.openAuthModal('login')); - document.getElementById('signupBtn').addEventListener('click', () => this.openAuthModal('signup')); - document.getElementById('logoutBtn').addEventListener('click', () => this.handleLogout()); - document.getElementById('authForm').addEventListener('submit', (e) => this.handleAuthSubmit(e)); - document.getElementById('authCancelBtn').addEventListener('click', () => this.closeAuthModal()); - document.getElementById('authSwitchLink').addEventListener('click', (e) => { - e.preventDefault(); - this.toggleAuthMode(); + bindTabEvents() { + const fileTabBtn = document.getElementById('fileTabBtn'); + const categoryTabBtn = document.getElementById('categoryTabBtn'); + const fileTab = document.getElementById('fileTab'); + const categoryTab = document.getElementById('categoryTab'); + + if (fileTabBtn && categoryTabBtn && fileTab && categoryTab) { + fileTabBtn.addEventListener('click', () => { + // 탭 버튼 활성화 상태 변경 + fileTabBtn.classList.add('active'); + categoryTabBtn.classList.remove('active'); + + // 탭 컨텐츠 표시/숨김 + fileTab.classList.add('active'); + categoryTab.classList.remove('active'); }); - } else { - // 오프라인 모드에서는 로그인 버튼 클릭 차단 - document.getElementById('loginBtn').addEventListener('click', (e) => { - e.preventDefault(); - alert('현재 오프라인 모드입니다. 로그인 기능을 사용할 수 없습니다.'); + + categoryTabBtn.addEventListener('click', () => { + // 탭 버튼 활성화 상태 변경 + categoryTabBtn.classList.add('active'); + fileTabBtn.classList.remove('active'); + + // 탭 컨텐츠 표시/숨김 + categoryTab.classList.add('active'); + fileTab.classList.remove('active'); + + // 카테고리 목록 렌더링 + this.renderCategoryList(); }); - document.getElementById('signupBtn').addEventListener('click', (e) => { + } + } + + bindCategoryEvents() { + // 카테고리 추가 폼 + const categoryForm = document.getElementById('categoryForm'); + if (categoryForm) { + categoryForm.addEventListener('submit', (e) => { e.preventDefault(); - alert('현재 오프라인 모드입니다. 회원가입 기능을 사용할 수 없습니다.'); + this.handleAddCategory(); }); } + // 카테고리 취소 버튼 + const cancelCategoryBtn = document.getElementById('cancelCategoryBtn'); + if (cancelCategoryBtn) { + cancelCategoryBtn.addEventListener('click', () => this.resetCategoryForm()); + } + // 모달 이벤트 - window.addEventListener('click', (e) => { - if (e.target === document.getElementById('editModal')) { - this.closeEditModal(); - } - if (e.target === document.getElementById('authModal')) { - this.closeAuthModal(); - } - }); + this.bindModalEvents(); } - generateId() { - return Date.now().toString(36) + Math.random().toString(36).substr(2); - } - - // 파일 확장자 추출 (안전한 형태로) - getFileExtension(fileName) { - const lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex > 0 && lastDotIndex < fileName.length - 1) { - return fileName.substring(lastDotIndex).toLowerCase(); + bindModalEvents() { + // 수정 모달 닫기 + const closeModal = document.getElementById('closeModal'); + if (closeModal) { + closeModal.addEventListener('click', () => { + document.getElementById('editModal').style.display = 'none'; + }); + } + + // 카테고리 수정 모달 닫기 + const closeCategoryModal = document.getElementById('closeCategoryModal'); + if (closeCategoryModal) { + closeCategoryModal.addEventListener('click', () => { + document.getElementById('editCategoryModal').style.display = 'none'; + }); + } + + // 수정 폼 제출 + const editForm = document.getElementById('editForm'); + if (editForm) { + editForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleUpdateFile(); + }); + } + + // 카테고리 수정 폼 제출 + const editCategoryForm = document.getElementById('editCategoryForm'); + if (editCategoryForm) { + editCategoryForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleUpdateCategory(); + }); } - return ''; // 확장자가 없는 경우 } - // 브라우저별 다운로드 폴더 경로 추정 - getDownloadFolderPath() { - const userAgent = navigator.userAgent.toLowerCase(); - const platform = navigator.platform.toLowerCase(); + bindFileEvents() { + // 파일 추가 폼 + const fileForm = document.getElementById('fileForm'); + if (fileForm) { + fileForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleAddFile(); + }); + } + + // 취소 버튼 + const cancelBtn = document.getElementById('cancelBtn'); + if (cancelBtn) { + cancelBtn.addEventListener('click', () => this.resetForm()); + } + + // 파일 업로드 영역 + this.setupFileUpload(); + } + + bindEditModalEvents() { + console.log('bindEditModalEvents 호출됨, 이미 바인딩된 상태:', this.editModalEventsBound); - if (platform.includes('win')) { - return '다운로드 폴더 (C:\\Users\\사용자명\\Downloads)'; - } else if (platform.includes('mac')) { - return '다운로드 폴더 (~/Downloads)'; - } else if (platform.includes('linux')) { - return '다운로드 폴더 (~/Downloads)'; - } else { - return '브라우저 기본 다운로드 폴더'; + // 이벤트가 이미 바인딩되었는지 확인 + if (this.editModalEventsBound) { + console.log('이미 바인딩됨, 스킵'); + return; } - } - - // 인증 관련 메서드들 - async initializeAuth() { - try { - const user = await getCurrentUser(); - if (user) { - this.currentUser = user; - this.updateAuthUI(true); - await this.loadUserFiles(); - this.setupRealtimeSubscription(); - } else { - this.updateAuthUI(false); - // 게스트 모드: 공개 파일 로드 - await this.loadPublicFiles(); - this.showGuestMode(); + + // 기존 이벤트 리스너 제거 (중복 방지) + const editForm = document.getElementById('editForm'); + if (editForm && this.editFormHandler) { + editForm.removeEventListener('submit', this.editFormHandler); + } + + // 수정 폼 제출 이벤트 핸들러 생성 및 바인딩 + this.editFormHandler = (e) => { + e.preventDefault(); + console.log('editForm 제출됨'); + + // 중복 실행 방지 + if (this.isUpdating) { + console.log('이미 업데이트 중입니다.'); + return; } - - setupAuthListener((event, session) => { - if (event === 'SIGNED_IN') { - this.currentUser = session.user; - this.updateAuthUI(true); - this.loadUserFiles(); - this.setupRealtimeSubscription(); - } else if (event === 'SIGNED_OUT') { - this.currentUser = null; - this.updateAuthUI(false); - this.loadPublicFiles(); - this.showGuestMode(); - this.cleanupRealtimeSubscription(); + + this.handleUpdateFile(); + }; + + if (editForm) { + console.log('editForm 이벤트 바인딩'); + editForm.addEventListener('submit', this.editFormHandler); + } + + // 수정 모달 닫기 + const closeModal = document.getElementById('closeModal'); + if (closeModal) { + console.log('closeModal 이벤트 바인딩'); + closeModal.addEventListener('click', () => { + console.log('closeModal 클릭됨'); + document.getElementById('editModal').style.display = 'none'; + this.currentEditId = null; + this.filesToDelete = []; + + // 새 파일 미리보기 초기화 + this.updateNewFilesPreview([]); + const newAttachmentsInput = document.getElementById('newAttachments'); + if (newAttachmentsInput) { + newAttachmentsInput.value = ''; } }); - } catch (error) { - console.error('인증 초기화 오류:', error); - } - } - - updateAuthUI(isAuthenticated) { - const authButtons = document.getElementById('authButtons'); - const userInfo = document.getElementById('userInfo'); - const userEmail = document.getElementById('userEmail'); - const formSection = document.querySelector('.form-section'); - - if (isAuthenticated && this.currentUser) { - authButtons.style.display = 'none'; - userInfo.style.display = 'flex'; - userEmail.textContent = this.currentUser.email; - formSection.style.display = 'block'; - this.updateSyncStatus(); - this.hideGuestMode(); - } else { - authButtons.style.display = 'flex'; - userInfo.style.display = 'none'; - formSection.style.display = 'none'; - } - } - - updateSyncStatus(status = 'auto') { - const syncStatusElement = document.getElementById('syncStatus'); - if (!syncStatusElement) return; - - // 자동 상태 판단 - if (status === 'auto') { - if (!isSupabaseConfigured()) { - status = 'offline'; - } else if (this.currentUser) { - status = 'online'; - } else { - status = 'offline'; - } - } - - // 상태 업데이트 - syncStatusElement.className = `sync-status ${status}`; - switch (status) { - case 'online': - syncStatusElement.textContent = '🟢 온라인'; - break; - case 'offline': - syncStatusElement.textContent = '🟡 오프라인'; - break; - case 'syncing': - syncStatusElement.textContent = '🔄 동기화 중'; - break; - case 'error': - syncStatusElement.textContent = '🔴 오류'; - break; - } - } - - openAuthModal(mode) { - // 오프라인 모드에서는 모달 열지 않음 - if (!window.supabase) { - alert('현재 오프라인 모드입니다. 로그인 기능을 사용할 수 없습니다.'); - return; } - this.authMode = mode; - const modal = document.getElementById('authModal'); - const title = document.getElementById('authModalTitle'); - const submitBtn = document.getElementById('authSubmitBtn'); - const confirmPasswordGroup = document.getElementById('confirmPasswordGroup'); - const switchText = document.getElementById('authSwitchText'); - - if (mode === 'signup') { - title.textContent = '👤 회원가입'; - submitBtn.textContent = '👤 회원가입'; - confirmPasswordGroup.style.display = 'block'; - switchText.innerHTML = '이미 계정이 있으신가요? 로그인하기'; + // 파일 선택 버튼과 드래그&드롭 영역 + const fileSelectBtn = document.getElementById('fileSelectBtn'); + const fileInput = document.getElementById('newAttachments'); + const dropZone = document.getElementById('fileDropZone'); + + if (fileSelectBtn && fileInput && dropZone) { + console.log('새로운 파일 선택 인터페이스 이벤트 바인딩'); + + // 파일 선택 버튼 클릭 이벤트 + fileSelectBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('파일 선택 버튼 클릭됨'); + fileInput.click(); + }); + + // 드롭존 클릭 이벤트 + dropZone.addEventListener('click', (e) => { + if (e.target === dropZone || e.target.closest('.drop-zone-content')) { + fileInput.click(); + } + }); + + // 파일 입력 변경 이벤트 + fileInput.addEventListener('change', (e) => { + console.log('파일 선택됨, 개수:', e.target.files.length); + this.updateNewFilesPreview(e.target.files); + }); + + // 드래그&드롭 이벤트 바인딩 + this.bindDragAndDropEvents(dropZone, fileInput); + } else { - title.textContent = '🔑 로그인'; - submitBtn.textContent = '🔑 로그인'; - confirmPasswordGroup.style.display = 'none'; - switchText.innerHTML = '계정이 없으신가요? 회원가입하기'; + console.error('새로운 파일 선택 요소들을 찾을 수 없음:', { + fileSelectBtn: !!fileSelectBtn, + fileInput: !!fileInput, + dropZone: !!dropZone + }); } - - // 이벤트 리스너 재바인딩 - const newSwitchLink = document.getElementById('authSwitchLink'); - newSwitchLink.addEventListener('click', (e) => { + + // 바인딩 완료 플래그 설정 + this.editModalEventsBound = true; + console.log('이벤트 바인딩 완료'); + } + + // 드래그&드롭 이벤트 바인딩 + bindDragAndDropEvents(dropZone, fileInput) { + console.log('드래그&드롭 이벤트 바인딩 시작'); + + // 드래그 진입 + dropZone.addEventListener('dragenter', (e) => { e.preventDefault(); - this.toggleAuthMode(); + e.stopPropagation(); + dropZone.classList.add('dragover'); }); - - modal.style.display = 'block'; - } - - closeAuthModal() { - document.getElementById('authModal').style.display = 'none'; - document.getElementById('authForm').reset(); - document.getElementById('authLoading').style.display = 'none'; - } - - toggleAuthMode() { - this.authMode = this.authMode === 'login' ? 'signup' : 'login'; - this.openAuthModal(this.authMode); - } - - async handleAuthSubmit(e) { - e.preventDefault(); - // 강제 오프라인 모드 - 모든 인증 시도 차단 - if (this.isOfflineMode || !window.supabase) { - alert('현재 오프라인 모드입니다. 로그인 기능을 사용할 수 없습니다.'); - this.closeAuthModal(); + // 드래그 오버 + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.classList.add('dragover'); + }); + + // 드래그 나감 + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // 완전히 벗어났을 때만 클래스 제거 + if (!dropZone.contains(e.relatedTarget)) { + dropZone.classList.remove('dragover'); + } + }); + + // 파일 드롭 + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.classList.remove('dragover'); + + const files = e.dataTransfer.files; + console.log('파일 드롭됨, 개수:', files.length); + + if (files.length > 0) { + // 파일 입력에 드롭된 파일들 설정 + fileInput.files = files; + this.updateNewFilesPreview(files); + } + }); + + console.log('드래그&드롭 이벤트 바인딩 완료'); + } + + // 새로운 파일 미리보기 업데이트 + updateNewFilesPreview(files) { + const previewContainer = document.getElementById('newFilesPreview'); + + if (!files || files.length === 0) { + previewContainer.classList.remove('show'); + previewContainer.innerHTML = ''; return; } - const email = document.getElementById('authEmail').value.trim(); - const password = document.getElementById('authPassword').value; - const confirmPassword = document.getElementById('authConfirmPassword').value; + previewContainer.classList.add('show'); + previewContainer.innerHTML = ''; + Array.from(files).forEach((file, index) => { + const fileItem = document.createElement('div'); + fileItem.className = 'preview-file-item'; + + const fileIcon = this.getFileIcon(file.name); + const fileSize = this.formatFileSize(file.size); + + fileItem.innerHTML = ` +
+
${fileIcon}
+
+
+ ${this.escapeHtml(file.name)} +
+
${fileSize}
+
+
+ + `; + + // 제거 버튼 이벤트 + const removeBtn = fileItem.querySelector('.preview-file-remove'); + removeBtn.addEventListener('click', () => { + this.removeFileFromPreview(index); + }); + + previewContainer.appendChild(fileItem); + }); + } + + // 파일 미리보기에서 제거 + removeFileFromPreview(indexToRemove) { + const fileInput = document.getElementById('newAttachments'); + const dt = new DataTransfer(); + + Array.from(fileInput.files).forEach((file, index) => { + if (index !== indexToRemove) { + dt.items.add(file); + } + }); + + fileInput.files = dt.files; + this.updateNewFilesPreview(fileInput.files); + } + + // 새 첨부파일 미리보기 표시 (기존 함수 - 호환성 유지) + showAttachmentPreview(files) { + const container = document.querySelector('.attachment-preview'); + + if (!container) { + // 미리보기 컨테이너가 없으면 생성 + const previewDiv = document.createElement('div'); + previewDiv.className = 'attachment-preview'; + document.querySelector('.new-attachment-section').appendChild(previewDiv); + } + + const preview = document.querySelector('.attachment-preview'); + + if (files.length === 0) { + preview.style.display = 'none'; + return; + } + + let previewText = `선택된 파일 (${files.length}개): `; + const fileNames = Array.from(files).map(file => file.name).slice(0, 3); + if (files.length > 3) { + fileNames.push(`외 ${files.length - 3}개`); + } + previewText += fileNames.join(', '); + + preview.innerHTML = previewText; + preview.style.display = 'block'; + } + + bindPaginationEvents() { + const prevBtn = document.getElementById('prevPage'); + const nextBtn = document.getElementById('nextPage'); + + if (prevBtn) prevBtn.addEventListener('click', () => this.goToPrevPage()); + if (nextBtn) nextBtn.addEventListener('click', () => this.goToNextPage()); + } + + setupFileUpload() { + const fileUploadArea = document.getElementById('fileUploadArea'); + const fileUpload = document.getElementById('fileUpload'); + + if (fileUploadArea && fileUpload) { + // 클릭으로 파일 선택 + fileUploadArea.addEventListener('click', () => { + fileUpload.click(); + }); + + // 파일 선택 시 미리보기 + fileUpload.addEventListener('change', (e) => { + this.handleFileSelection(e.target.files); + }); + + // 드래그 앤 드롭 + fileUploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + fileUploadArea.classList.add('drag-over'); + }); + + fileUploadArea.addEventListener('dragleave', () => { + fileUploadArea.classList.remove('drag-over'); + }); + + fileUploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + fileUploadArea.classList.remove('drag-over'); + this.handleFileSelection(e.dataTransfer.files); + }); + } + } + + async checkSession() { + try { + const response = await fetch('/api/auth/session'); + if (response.ok) { + const data = await response.json(); + if (data.user) { + this.currentUser = data.user; + this.isLoggedIn = true; + await this.loadData(); + } + } + } catch (error) { + console.log('세션 확인 실패:', error); + } + } + + async handleLogin() { + const email = document.getElementById('adminEmail').value.trim(); + const password = document.getElementById('adminPassword').value; + const loginBtn = document.getElementById('loginBtn'); + if (!email || !password) { - alert('이메일과 비밀번호를 입력해주세요.'); + this.showNotification('이메일과 비밀번호를 입력해주세요.', 'error'); return; } - if (this.authMode === 'signup' && password !== confirmPassword) { - alert('비밀번호가 일치하지 않습니다.'); - return; - } - - this.showAuthLoading(true); - try { - if (this.authMode === 'signup') { - await signUp(email, password); - alert('회원가입이 완료되었습니다! 이메일을 확인해주세요.'); + loginBtn.disabled = true; + loginBtn.textContent = '로그인 중...'; + + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email, password }) + }); + + const data = await response.json(); + + if (response.ok) { + this.currentUser = data.user; + this.isLoggedIn = true; + this.showNotification('로그인되었습니다!', 'success'); + + await this.loadData(); + this.updateUI(); } else { - await signIn(email, password); + throw new Error(data.message || '로그인에 실패했습니다.'); } - this.closeAuthModal(); } catch (error) { - console.error('인증 오류:', error); - alert(`${this.authMode === 'signup' ? '회원가입' : '로그인'} 중 오류가 발생했습니다: ${error.message}`); + console.error('로그인 오류:', error); + this.showNotification(error.message || '로그인 중 오류가 발생했습니다.', 'error'); } finally { - this.showAuthLoading(false); + loginBtn.disabled = false; + loginBtn.textContent = '로그인'; } } async handleLogout() { - // 오프라인 모드에서는 로그아웃 불가 - if (!window.supabase) { - alert('현재 오프라인 모드입니다. 로그아웃 기능을 사용할 수 없습니다.'); - return; - } - try { - await signOut(); - this.showNotification('로그아웃되었습니다.', 'success'); + await fetch('/api/auth/logout', { method: 'POST' }); + + this.currentUser = null; + this.isLoggedIn = false; + this.files = []; + this.categories = []; + + this.showNotification('로그아웃되었습니다.', 'info'); + this.updateUI(); + + // 폼 초기화 + document.getElementById('adminPassword').value = ''; } catch (error) { console.error('로그아웃 오류:', error); - alert('로그아웃 중 오류가 발생했습니다.'); + this.showNotification('로그아웃 중 오류가 발생했습니다.', 'error'); } } - showAuthLoading(show) { - const loading = document.getElementById('authLoading'); - const form = document.getElementById('authForm'); + async loadData() { + if (!this.isLoggedIn) return; + + try { + // 파일 목록 로드 + const filesData = await window.AdminAPI.Files.getAll(); + this.files = filesData.data || []; + console.log('파일 로드 완료:', this.files.length, '개'); + + // 카테고리 목록 로드 + const categoriesData = await window.AdminAPI.Categories.getAll(); + this.categories = categoriesData.data || []; + console.log('카테고리 로드 완료:', this.categories.length, '개'); + console.log('카테고리 데이터:', this.categories); + + this.filteredFiles = [...this.files]; + this.renderFiles(); + this.updatePagination(); + this.updateCategoryOptions(); + + } catch (error) { + console.error('데이터 로드 오류:', error); + this.showNotification('데이터 로드 중 오류가 발생했습니다.', 'error'); + } + } + + updateUI() { + const loginSection = document.getElementById('loginSection'); + const adminSection = document.getElementById('adminSection'); + const adminPanel = document.getElementById('adminPanel'); + const adminUserEmail = document.getElementById('adminUserEmail'); + + if (this.isLoggedIn) { + // 로그인 상태 + if (loginSection) loginSection.style.display = 'none'; + if (adminSection) adminSection.style.display = 'flex'; + if (adminPanel) adminPanel.style.display = 'block'; + if (adminUserEmail) adminUserEmail.textContent = this.currentUser.email; + } else { + // 로그아웃 상태 + if (loginSection) loginSection.style.display = 'flex'; + if (adminSection) adminSection.style.display = 'none'; + if (adminPanel) adminPanel.style.display = 'none'; + } + } + + updateCategoryOptions() { + const categorySelects = ['fileCategory', 'categoryFilter', 'editCategory']; - if (show) { - loading.style.display = 'block'; - form.style.display = 'none'; - } else { - loading.style.display = 'none'; - form.style.display = 'block'; - } + categorySelects.forEach(selectId => { + const select = document.getElementById(selectId); + if (!select) return; + + // 기존 옵션 제거 (첫 번째 옵션 제외) + while (select.children.length > 1) { + select.removeChild(select.lastChild); + } + + // 카테고리 옵션 추가 + this.categories.forEach(category => { + const option = document.createElement('option'); + option.value = category.name; + option.textContent = category.name; + select.appendChild(option); + }); + }); } - // 인증 함수 완전 차단 - overrideAuthFunctions() { - // 전역 인증 함수들을 빈 함수로 덮어씌움 - if (typeof signIn === 'function') { - window.signIn = () => { - throw new Error('오프라인 모드에서는 로그인할 수 없습니다.'); - }; - } - if (typeof signUp === 'function') { - window.signUp = () => { - throw new Error('오프라인 모드에서는 회원가입할 수 없습니다.'); - }; - } - if (typeof signOut === 'function') { - window.signOut = () => { - throw new Error('오프라인 모드에서는 로그아웃할 수 없습니다.'); - }; - } - if (typeof getCurrentUser === 'function') { - window.getCurrentUser = () => Promise.resolve(null); - } - } - - // 인증 UI 업데이트 - updateAuthUI(isAuthenticated = true) { - const authButtons = document.getElementById('authButtons'); - const userInfo = document.getElementById('userInfo'); - const userEmail = document.getElementById('userEmail'); - const formSection = document.querySelector('.form-section'); + handleSearch() { + const searchTerm = document.getElementById('searchInput').value.toLowerCase(); + const categoryFilter = document.getElementById('categoryFilter').value; - // 오프라인 모드에서는 항상 로그인된 것처럼 처리 - if ((isAuthenticated && this.currentUser) || !window.supabase) { - if (authButtons) authButtons.style.display = 'none'; - if (userInfo) userInfo.style.display = 'flex'; - if (userEmail) userEmail.textContent = this.currentUser?.email || '오프라인 사용자'; - if (formSection) formSection.style.display = 'block'; - if (window.supabase && this.updateSyncStatus) { - this.updateSyncStatus('online'); - } - this.hideGuestMode(); - } else { - if (authButtons) authButtons.style.display = 'flex'; - if (userInfo) userInfo.style.display = 'none'; - if (formSection) formSection.style.display = 'none'; - this.showGuestMode(); + let filteredFiles = this.files; + + if (searchTerm) { + filteredFiles = filteredFiles.filter(file => + file.title.toLowerCase().includes(searchTerm) || + file.description.toLowerCase().includes(searchTerm) || + (file.tags && this.parseJsonTags(file.tags).some(tag => tag.toLowerCase().includes(searchTerm))) + ); } - } - - // 오프라인 모드 관련 - showOfflineMode() { - const container = document.querySelector('.container'); - const offlineNotice = document.createElement('div'); - offlineNotice.className = 'offline-mode'; - offlineNotice.innerHTML = '⚠️ 오프라인 모드: 로컬 저장소를 사용합니다. 로그인 없이 모든 기능을 사용할 수 있습니다.'; - container.insertBefore(offlineNotice, container.firstChild.nextSibling); - } - - // 게스트 모드 관련 - showGuestMode() { - this.hideGuestMode(); // 기존 알림 제거 - const container = document.querySelector('.container'); - const guestNotice = document.createElement('div'); - guestNotice.className = 'guest-mode'; - guestNotice.id = 'guestModeNotice'; - guestNotice.innerHTML = ` -
- 👤 오프라인 모드 - 로컬 저장소를 사용합니다 - ⚠️ 인터넷 연결 시 Supabase 기능을 사용할 수 있습니다 -
- `; - container.insertBefore(guestNotice, container.firstChild.nextSibling); - } - - hideGuestMode() { - const guestNotice = document.getElementById('guestModeNotice'); - if (guestNotice) { - guestNotice.remove(); - } - } - - // 공개 파일 로드 (게스트용) - async loadPublicFiles() { - if (isSupabaseConfigured()) { - try { - // Supabase에서 모든 파일 로드 (RLS로 공개 파일만 접근 가능) - const data = await SupabaseHelper.getFiles('public'); - this.files = data.map(file => ({ - ...file, - files: file.file_attachments || [], - isReadOnly: true - })); - } catch (error) { - console.error('공개 파일 로딩 오류:', error); - // localStorage 폴백 - this.files = this.loadFiles().map(file => ({ ...file, isReadOnly: true })); - } - } else { - // 오프라인 모드: localStorage의 파일을 읽기 전용으로 로드 - this.files = this.loadFiles().map(file => ({ ...file, isReadOnly: true })); + + if (categoryFilter) { + filteredFiles = filteredFiles.filter(file => file.category === categoryFilter); } + + this.filteredFiles = filteredFiles; + this.currentPage = 1; this.renderFiles(); - this.updateEmptyState(); + this.updatePagination(); } - setupOnlineStatusListener() { - window.addEventListener('online', () => { - this.isOnline = true; - this.showNotification('온라인 상태가 되었습니다.', 'success'); - }); - - window.addEventListener('offline', () => { - this.isOnline = false; - this.showNotification('오프라인 상태입니다.', 'info'); - }); + parseJsonTags(tags) { + try { + if (typeof tags === 'string') { + return JSON.parse(tags); + } + return Array.isArray(tags) ? tags : []; + } catch (error) { + return []; + } } - // Supabase 데이터베이스 연동 메서드들 - async loadUserFiles() { - if (!this.currentUser || !isSupabaseConfigured()) { - this.files = this.loadFiles(); // localStorage 폴백 - this.updateSyncStatus('offline'); + renderFiles() { + const fileList = document.getElementById('fileList'); + const sortBy = document.getElementById('sortBy').value; + + if (!fileList) return; + + // 정렬 + const sortedFiles = [...this.filteredFiles].sort((a, b) => { + switch (sortBy) { + case 'title': + return a.title.localeCompare(b.title); + case 'category': + return a.category.localeCompare(b.category); + case 'date': + default: + return new Date(b.created_at) - new Date(a.created_at); + } + }); + + // 페이지네이션 적용 + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + const paginatedFiles = sortedFiles.slice(startIndex, endIndex); + + if (sortedFiles.length === 0) { + fileList.innerHTML = ` + + 📂 조건에 맞는 자료가 없습니다. + + `; return; } - try { - this.updateSyncStatus('syncing'); - const data = await SupabaseHelper.getFiles(this.currentUser.id); - this.files = data.map(file => ({ - ...file, - files: file.file_attachments || [] // 첨부파일 정보 매핑 - })); - this.renderFiles(); - this.updateEmptyState(); - this.updateSyncStatus('online'); - } catch (error) { - console.error('파일 로딩 오류:', error); - this.showNotification('파일을 불러오는 중 오류가 발생했습니다.', 'error'); - this.updateSyncStatus('error'); - // 오류 시 localStorage 폴백 - this.files = this.loadFiles(); - } + fileList.innerHTML = paginatedFiles.map((file, index) => + this.createFileRowHTML(file, startIndex + index + 1) + ).join(''); } - async addFileToSupabase(fileData) { - if (!this.currentUser || !isSupabaseConfigured()) { - return this.addFileLocally(fileData); - } - - try { - this.updateSyncStatus('syncing'); - console.log('파일 데이터 추가 중...', fileData); - - const result = await SupabaseHelper.addFile(fileData, this.currentUser.id); - console.log('파일 데이터 추가 성공:', result); - - // 첨부파일이 있는 경우 파일 업로드 처리 - if (fileData.files && fileData.files.length > 0) { - console.log(`${fileData.files.length}개의 첨부파일 업로드 시작...`); - await this.uploadAttachments(result.id, fileData.files); - console.log('모든 첨부파일 업로드 완료'); - } - - this.showNotification('새 자료가 성공적으로 추가되었습니다!', 'success'); - await this.loadUserFiles(); // 목록 새로고침 - this.updateSyncStatus('online'); - this.clearForm(); // 폼 초기화 - - } catch (error) { - console.error('파일 추가 오류:', error); - - // 더 구체적인 에러 메시지 제공 - let errorMessage = '파일 추가 중 오류가 발생했습니다.'; - if (error.message) { - errorMessage += ` (${error.message})`; - } - - this.showNotification(errorMessage, 'error'); - this.updateSyncStatus('error'); - - // 콘솔에 상세 오류 정보 출력 - if (error.details) { - console.error('오류 상세:', error.details); - } - if (error.hint) { - console.error('오류 힌트:', error.hint); - } - } - } - - async updateFileInSupabase(id, updates) { - if (!this.currentUser || !isSupabaseConfigured()) { - return this.updateFileLocally(id, updates); - } - - try { - await SupabaseHelper.updateFile(id, updates, this.currentUser.id); - this.showNotification('자료가 성공적으로 수정되었습니다!', 'success'); - await this.loadUserFiles(); // 목록 새로고침 - } catch (error) { - console.error('파일 수정 오류:', error); - this.showNotification('파일 수정 중 오류가 발생했습니다.', 'error'); - } - } - - async deleteFileFromSupabase(id) { - if (!this.currentUser || !isSupabaseConfigured()) { - return this.deleteFileLocally(id); - } - - try { - // 첨부파일들을 Storage에서 삭제 - await this.deleteAttachmentsFromStorage(id); - - // 데이터베이스에서 파일 삭제 (CASCADE로 첨부파일 정보도 함께 삭제) - await SupabaseHelper.deleteFile(id, this.currentUser.id); - this.showNotification('자료가 성공적으로 삭제되었습니다!', 'success'); - await this.loadUserFiles(); // 목록 새로고침 - } catch (error) { - console.error('파일 삭제 오류:', error); - this.showNotification('파일 삭제 중 오류가 발생했습니다.', 'error'); - } - } - - // localStorage 폴백 메서드들 - addFileLocally(fileData) { - // 로컬 저장용 데이터 생성 (ID와 타임스탬프 추가) - const localFileData = { - id: this.generateId(), - ...fileData, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }; - - // 첨부파일이 있는 경우, localStorage에 바로 저장 가능한 형태로 처리 - if (fileData.files && fileData.files.length > 0) { - // 첨부파일 데이터가 이미 base64 형태로 준비되어 있으므로 그대로 사용 - localFileData.files = fileData.files.map(file => ({ - name: file.name, - original_name: file.name, - size: file.size, - type: file.type, - data: file.data // base64 데이터 - })); - } else { - localFileData.files = []; - } + createFileRowHTML(file, rowNumber) { + const createdDate = new Date(file.created_at).toLocaleDateString('ko-KR'); + const hasAttachments = file.files && file.files.length > 0; + const tags = this.parseJsonTags(file.tags); - this.files.push(localFileData); - this.saveFiles(); - this.renderFiles(); - this.updateEmptyState(); - this.showNotification('새 자료가 성공적으로 추가되었습니다! (로컬 저장)', 'success'); - this.clearForm(); // 폼 초기화 - } - - updateFileLocally(id, updates) { - const fileIndex = this.files.findIndex(f => f.id === id); - if (fileIndex !== -1) { - this.files[fileIndex] = { - ...this.files[fileIndex], - ...updates, - updated_at: new Date().toISOString() - }; - this.saveFiles(); - this.renderFiles(); - this.showNotification('자료가 성공적으로 수정되었습니다! (로컬 저장)', 'success'); - } - } - - deleteFileLocally(id) { - this.files = this.files.filter(f => f.id !== id); - this.saveFiles(); - this.renderFiles(); - this.updateEmptyState(); - this.showNotification('자료가 성공적으로 삭제되었습니다! (로컬 저장)', 'success'); - } - - // 실시간 구독 관련 - setupRealtimeSubscription() { - if (!this.currentUser || !isSupabaseConfigured()) return; - - this.realtimeSubscription = SupabaseHelper.subscribeToFiles( - this.currentUser.id, - (payload) => { - console.log('실시간 업데이트:', payload); - this.loadUserFiles(); // 변경사항이 있으면 목록 새로고침 - } - ); - } - - cleanupRealtimeSubscription() { - if (this.realtimeSubscription) { - this.realtimeSubscription.unsubscribe(); - this.realtimeSubscription = null; - } - } - - // 파일 업로드 관련 메서드들 - async uploadAttachments(fileId, attachments) { - if (!isSupabaseConfigured() || !this.currentUser) { - console.log('오프라인 모드: 첨부파일을 base64로 저장합니다.'); - return; // 오프라인 모드에서는 base64로 저장된 상태 유지 - } - - const uploadedFiles = []; - - try { - for (let i = 0; i < attachments.length; i++) { - const attachment = attachments[i]; - - try { - console.log(`파일 업로드 중... (${i + 1}/${attachments.length}): ${attachment.name}`); - - // base64 데이터를 Blob으로 변환 - const response = await fetch(attachment.data); - const blob = await response.blob(); - - // 안전한 파일명 생성 (고유한 이름으로 저장, 원본명은 DB에 저장) - const fileExtension = this.getFileExtension(attachment.name); - const safeFileName = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}${fileExtension}`; - const filePath = `${this.currentUser.id}/${fileId}/${safeFileName}`; - - // Storage 버킷 확인 - const bucketExists = await SupabaseHelper.checkOrCreateBucket(); - if (!bucketExists) { - throw new Error('Storage 버킷 접근 권한이 없습니다. Storage 정책을 확인해주세요.'); + return ` + + ${rowNumber} + + ${file.category} + + +
+ + ${this.escapeHtml(file.title)} + + ${file.description ? `
${this.escapeHtml(file.description)}` : ''} + ${tags.length > 0 ? + `
${tags.map(tag => `#${this.escapeHtml(tag)}`).join('')}
` : '' + } +
+ + + ${hasAttachments ? + `
+ ${file.files.map((f, index) => + `
+ ${this.getFileIcon(f.original_name || 'unknown')} + ${this.escapeHtml(f.original_name || '파일')} +
` + ).join('')} +
` : + `-` } - - // Supabase Storage에 업로드 - const uploadResult = await SupabaseHelper.uploadFile(blob, filePath); - console.log('Storage 업로드 성공:', uploadResult); - - // 데이터베이스에 첨부파일 정보 저장 - const attachmentResult = await this.addFileAttachment(fileId, { - original_name: attachment.name, - storage_path: filePath, - file_size: attachment.size || blob.size, - mime_type: attachment.type || blob.type - }); - - uploadedFiles.push(attachmentResult); - console.log('첨부파일 정보 저장 성공:', attachmentResult); - - } catch (fileError) { - console.error(`파일 "${attachment.name}" 업로드 실패:`, fileError); - throw new Error(`파일 "${attachment.name}" 업로드에 실패했습니다: ${fileError.message}`); - } - } - - console.log('모든 첨부파일 업로드 완료:', uploadedFiles); - return uploadedFiles; - - } catch (error) { - console.error('파일 업로드 오류:', error); - - // 부분적으로 업로드된 파일들 정리 (선택사항) - try { - for (const uploadedFile of uploadedFiles) { - if (uploadedFile.storage_path) { - await SupabaseHelper.deleteStorageFile(uploadedFile.storage_path); + + ${createdDate} + + + ${hasAttachments ? + `` : + '' } - } - } catch (cleanupError) { - console.error('업로드 실패 파일 정리 중 오류:', cleanupError); - } - - throw error; - } + + + + `; } - async addFileAttachment(fileId, attachmentData) { - if (!isSupabaseConfigured()) { - return; // 오프라인 모드에서는 처리하지 않음 - } - - try { - // SupabaseHelper를 통해 첨부파일 정보 저장 - const result = await SupabaseHelper.addFileAttachment(fileId, attachmentData); - return result; - } catch (error) { - console.error('첨부파일 정보 저장 오류:', error); - throw error; - } - } - - async downloadFileFromStorage(filePath, originalName) { - if (!isSupabaseConfigured()) { - return; // 오프라인 모드에서는 처리하지 않음 - } - - try { - console.log('파일 다운로드 시도:', filePath, originalName); - - // Storage 버킷 확인 - const bucketExists = await SupabaseHelper.checkOrCreateBucket(); - if (!bucketExists) { - throw new Error('Storage 버킷 접근 권한이 없습니다. Storage 정책을 확인해주세요.'); - } - - const url = await SupabaseHelper.getFileUrl(filePath); - console.log('다운로드 URL 생성:', url); - - // 다운로드 링크 생성 - const link = document.createElement('a'); - link.href = url; - link.download = originalName; - - // Ctrl/Cmd 키를 누른 상태에서 클릭하면 "다른 이름으로 저장" 대화상자 표시 - if (window.event && (window.event.ctrlKey || window.event.metaKey)) { - link.target = '_blank'; - // 브라우저의 다운로드 관리자로 보내기 - } - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - console.log('파일 다운로드 완료:', originalName); - } catch (error) { - console.error('파일 다운로드 오류:', error); - - // 더 구체적인 오류 메시지 제공 - let errorMessage = '파일 다운로드 중 오류가 발생했습니다.'; - if (error.message.includes('Bucket not found')) { - errorMessage = 'Storage 버킷이 설정되지 않았습니다. 관리자에게 문의하세요.'; - } else if (error.message.includes('파일을 찾을 수 없습니다')) { - errorMessage = '파일을 찾을 수 없습니다. 파일이 삭제되었을 수 있습니다.'; - } - - this.showNotification(errorMessage, 'error'); - } - } - - async deleteAttachmentsFromStorage(fileId) { - if (!isSupabaseConfigured() || !this.currentUser) { - return; // 오프라인 모드에서는 처리하지 않음 - } - - try { - // 파일의 모든 첨부파일 경로 가져오기 - const { data: attachments, error } = await supabase - .from('file_attachments') - .select('storage_path') - .eq('file_id', fileId); - - if (error) throw error; - - // 각 파일을 Storage에서 삭제 - for (const attachment of attachments) { - await SupabaseHelper.deleteStorageFile(attachment.storage_path); - } - } catch (error) { - console.error('첨부파일 삭제 오류:', error); - } - } - - handleSubmit(e) { - e.preventDefault(); - - if (!this.currentUser) { + async handleAddFile() { + if (!this.isLoggedIn) { this.showNotification('로그인이 필요합니다.', 'error'); return; } - + const title = document.getElementById('fileTitle').value.trim(); const description = document.getElementById('fileDescription').value.trim(); const category = document.getElementById('fileCategory').value; - const tags = document.getElementById('fileTags').value.split(',').map(tag => tag.trim()).filter(tag => tag); + const tags = document.getElementById('fileTags').value.trim(); const fileInput = document.getElementById('fileUpload'); - + if (!title || !category) { - alert('제목과 카테고리는 필수 입력 항목입니다.'); + this.showNotification('제목과 카테고리는 필수입니다.', 'error'); return; } - const fileData = { - title, - description, - category, - tags, - files: [] // 첨부파일 임시 저장용 (Supabase 전송시 제외됨) - }; - - // this.selectedFiles 사용 (드래그앤드롭으로 선택한 파일들 포함) - if (this.selectedFiles && this.selectedFiles.length > 0) { - let processedFiles = 0; - Array.from(this.selectedFiles).forEach(file => { - const reader = new FileReader(); - reader.onload = (e) => { - fileData.files.push({ - name: file.name, - size: file.size, - type: file.type, - data: e.target.result - }); - - processedFiles++; - if (processedFiles === this.selectedFiles.length) { - this.addFileToSupabase(fileData); - } - }; - reader.readAsDataURL(file); - }); - } else { - this.addFileToSupabase(fileData); - } - } - - async addFile(fileData) { - // 호환성을 위해 유지, 실제로는 addFileToSupabase 사용 - await this.addFileToSupabase(fileData); - this.clearForm(); - } - - // 드래그 앤 드롭 설정 - setupDragAndDrop() { - const uploadArea = document.getElementById('fileUploadArea'); - const fileInput = document.getElementById('fileUpload'); - - // 드래그 이벤트 - uploadArea.addEventListener('dragover', (e) => { - e.preventDefault(); - uploadArea.classList.add('drag-over'); - }); - - uploadArea.addEventListener('dragleave', (e) => { - e.preventDefault(); - uploadArea.classList.remove('drag-over'); - }); - - uploadArea.addEventListener('drop', (e) => { - e.preventDefault(); - uploadArea.classList.remove('drag-over'); + try { + // 로딩 상태 표시 + this.showLoadingState('파일 등록 중...', true); - const files = Array.from(e.dataTransfer.files); - this.handleMultipleFiles(files); - }); - - // 클릭 이벤트 - uploadArea.addEventListener('click', () => { - fileInput.click(); - }); + const submitBtn = document.getElementById('submitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = '등록 중...'; + + const formData = new FormData(); + formData.append('title', title); + formData.append('description', description); + formData.append('category', category); + formData.append('tags', JSON.stringify(tags ? tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [])); + + // 파일 추가 + if (fileInput.files.length > 0) { + for (const file of fileInput.files) { + formData.append('files', file); + } + } + + const response = await fetch('/api/files', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (response.ok) { + this.showNotification('파일이 성공적으로 등록되었습니다!', 'success'); + this.resetForm(); + await this.loadData(); + } else { + throw new Error(data.message || '파일 등록에 실패했습니다.'); + } + } catch (error) { + console.error('파일 추가 오류:', error); + this.showNotification(error.message || '파일 등록 중 오류가 발생했습니다.', 'error'); + } finally { + // 로딩 상태 해제 및 버튼 복원 + this.hideLoadingState(); + const submitBtn = document.getElementById('submitBtn'); + submitBtn.disabled = false; + submitBtn.textContent = '📤 추가'; + } } - handleFileUpload(e) { - const files = Array.from(e.target.files); - this.handleMultipleFiles(files); - } - - handleMultipleFiles(files) { - this.selectedFiles = files; - this.updateFilePreview(); - } - - updateFilePreview() { - const container = document.getElementById('selectedFiles'); - - if (!this.selectedFiles || this.selectedFiles.length === 0) { - container.innerHTML = ''; + async deleteFile(id) { + if (!confirm('정말로 이 파일을 삭제하시겠습니까?')) { return; } + + try { + const response = await fetch(`/api/files/${id}`, { + method: 'DELETE' + }); + + if (response.ok) { + this.showNotification('파일이 삭제되었습니다.', 'success'); + await this.loadData(); + } else { + const data = await response.json(); + throw new Error(data.message || '파일 삭제에 실패했습니다.'); + } + } catch (error) { + console.error('파일 삭제 오류:', error); + this.showNotification(error.message || '파일 삭제 중 오류가 발생했습니다.', 'error'); + } + } + + editFile(id) { + const file = this.files.find(f => f.id === id); + if (!file) { + this.showNotification('파일을 찾을 수 없습니다.', 'error'); + return; + } + + // 수정 모달에 데이터 채우기 + document.getElementById('editTitle').value = file.title; + document.getElementById('editDescription').value = file.description || ''; + document.getElementById('editCategory').value = file.category; - const totalSize = this.selectedFiles.reduce((sum, file) => sum + file.size, 0); + const tags = this.parseJsonTags(file.tags); + document.getElementById('editTags').value = tags.join(', '); + + // 기존 첨부파일 표시 + this.renderExistingAttachments(file.files || []); - container.innerHTML = ` -
- 📎 선택된 파일: ${this.selectedFiles.length}개 (총 ${this.formatFileSize(totalSize)}) -
- ${this.selectedFiles.map((file, index) => ` -
-
-
${this.getFileIcon(file.name)}
-
-
${file.name}
-
${this.formatFileSize(file.size)}
-
+ // 첨부파일 삭제 목록 초기화 + this.filesToDelete = []; + + // 새 파일 입력 초기화 + const newAttachmentsInput = document.getElementById('newAttachments'); + if (newAttachmentsInput) { + newAttachmentsInput.value = ''; + } + + // 새 파일 미리보기 초기화 + this.updateNewFilesPreview([]); + + // 수정 모달 이벤트 바인딩 (한번만) + if (!this.editModalEventsBound) { + this.bindEditModalEvents(); + } + + this.currentEditId = id; + document.getElementById('editModal').style.display = 'flex'; + } + + // 기존 첨부파일 렌더링 + renderExistingAttachments(attachments) { + const container = document.getElementById('existingAttachments'); + const noFilesIndicator = document.getElementById('noExistingFiles'); + + if (!attachments || attachments.length === 0) { + noFilesIndicator.style.display = 'block'; + // 기존 파일 아이템들 제거 + const existingItems = container.querySelectorAll('.attachment-item'); + existingItems.forEach(item => item.remove()); + return; + } + + noFilesIndicator.style.display = 'none'; + + // 기존 아이템들 제거 + const existingItems = container.querySelectorAll('.attachment-item'); + existingItems.forEach(item => item.remove()); + + // 새로운 첨부파일 아이템들 추가 + attachments.forEach((file, index) => { + const fileIcon = this.getFileIcon(file.original_name); + const fileSize = this.formatFileSize(file.file_size); + + const attachmentItem = document.createElement('div'); + attachmentItem.className = 'attachment-item'; + attachmentItem.setAttribute('data-attachment-id', file.id); + + attachmentItem.innerHTML = ` +
+ ${fileIcon} +
+
${this.escapeHtml(file.original_name)}
+
${fileSize}
-
- `).join('')} - `; +
+ + +
+ `; + + container.appendChild(attachmentItem); + }); + } + + // 첨부파일 삭제 표시 + markAttachmentForDeletion(attachmentId, buttonElement) { + const attachmentItem = buttonElement.closest('.attachment-item'); + + if (!this.filesToDelete) { + this.filesToDelete = []; + } + + if (this.filesToDelete.includes(attachmentId)) { + // 삭제 취소 + this.filesToDelete = this.filesToDelete.filter(id => id !== attachmentId); + attachmentItem.style.opacity = '1'; + attachmentItem.style.textDecoration = 'none'; + buttonElement.innerHTML = '🗑️ 삭제'; + buttonElement.style.background = '#ef4444'; + } else { + // 삭제 표시 + this.filesToDelete.push(attachmentId); + attachmentItem.style.opacity = '0.5'; + attachmentItem.style.textDecoration = 'line-through'; + buttonElement.innerHTML = '↶ 취소'; + buttonElement.style.background = '#6b7280'; + } + } + + // 파일 크기 포맷팅 + formatFileSize(bytes) { + if (!bytes) return '0 B'; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; + } + + async handleUpdateFile() { + if (!this.currentEditId) { + this.showNotification('수정할 파일을 찾을 수 없습니다.', 'error'); + return; + } + + // 중복 실행 방지 플래그 설정 + if (this.isUpdating) { + console.log('이미 업데이트 중입니다.'); + return; + } + this.isUpdating = true; + + const title = document.getElementById('editTitle').value.trim(); + const description = document.getElementById('editDescription').value.trim(); + const category = document.getElementById('editCategory').value; + const tags = document.getElementById('editTags').value.trim(); + + if (!title || !category) { + this.isUpdating = false; // 플래그 해제 + this.showNotification('제목과 카테고리는 필수입니다.', 'error'); + return; + } + + try { + // 로딩 상태 표시 시작 + this.showLoadingState('수정 중...', true); + + // FormData를 사용하여 파일과 데이터를 함께 전송 + const formData = new FormData(); + formData.append('title', title); + formData.append('description', description); + formData.append('category', category); + formData.append('tags', JSON.stringify(tags ? tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [])); + + // 삭제할 첨부파일 ID들 + if (this.filesToDelete && this.filesToDelete.length > 0) { + formData.append('filesToDelete', JSON.stringify(this.filesToDelete)); + } + + // 새로 추가할 첨부파일들 + const newFileInput = document.getElementById('newAttachments'); + if (newFileInput && newFileInput.files.length > 0) { + for (const file of newFileInput.files) { + formData.append('files', file); + } + } + + // API 클라이언트를 사용하여 파일 업데이트 + const data = await window.AdminAPI.Files.update(this.currentEditId, formData); + + this.showNotification('파일이 성공적으로 수정되었습니다!', 'success'); + document.getElementById('editModal').style.display = 'none'; + this.currentEditId = null; + this.filesToDelete = []; + + // 새 파일 미리보기 초기화 + this.updateNewFilesPreview([]); + const newAttachmentsInput = document.getElementById('newAttachments'); + if (newAttachmentsInput) { + newAttachmentsInput.value = ''; + } + + await this.loadData(); + } catch (error) { + console.error('파일 수정 오류:', error); + this.showNotification(error.message || '파일 수정 중 오류가 발생했습니다.', 'error'); + } finally { + // 작업 완료 후 플래그 해제 및 로딩 상태 제거 + this.isUpdating = false; + this.hideLoadingState(); + } + } + + async downloadFiles(id) { + console.log('downloadFiles 호출됨:', id); + const file = this.files.find(f => f.id === id); + console.log('찾은 파일:', file); + + if (!file || !file.files || file.files.length === 0) { + console.log('첨부파일 없음'); + this.showNotification('첨부파일이 없습니다.', 'error'); + return; + } + + try { + console.log('다운로드 시작, 파일 개수:', file.files.length); + if (file.files.length === 1) { + // 단일 파일: 직접 다운로드 + console.log('단일 파일 다운로드'); + await this.downloadSingleFile(id, 0); + this.showNotification('파일 다운로드 완료', 'success'); + } else { + // 다중 파일: 각각 다운로드 + console.log('다중 파일 다운로드'); + for (let i = 0; i < file.files.length; i++) { + console.log(`파일 ${i + 1}/${file.files.length} 다운로드 중`); + await this.downloadSingleFile(id, i); + // 짧은 딜레이를 추가하여 브라우저가 다운로드를 처리할 시간을 줌 + await new Promise(resolve => setTimeout(resolve, 500)); + } + this.showNotification(`${file.files.length}개 파일 다운로드 완료`, 'success'); + } + } catch (error) { + console.error('파일 다운로드 오류:', error); + this.showNotification(`다운로드 오류: ${error.message}`, 'error'); + } + } + + async downloadSingleFile(fileId, attachmentIndex) { + try { + // 다운로드 시작 로딩 표시 + this.showLoadingState('다운로드 준비 중...', false); + + console.log('downloadSingleFile 호출됨:', fileId, attachmentIndex); + const file = this.files.find(f => f.id === fileId); + console.log('찾은 파일:', file); + + if (!file || !file.files[attachmentIndex]) { + console.log('파일 또는 첨부파일을 찾을 수 없음'); + throw new Error('파일을 찾을 수 없습니다.'); + } + + const attachmentId = file.files[attachmentIndex].id; + const downloadUrl = `/api/download/${fileId}/${attachmentId}`; + console.log('다운로드 URL:', downloadUrl); + + const response = await fetch(downloadUrl, { + credentials: 'include' + }); + console.log('응답 상태:', response.status, response.statusText); + + if (!response.ok) { + const errorText = await response.text(); + console.log('응답 오류:', errorText); + throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); + } + + console.log('다운로드 시작...'); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + // 파일명을 서버에서 전송된 정보에서 추출 (개선된 방식) + const contentDisposition = response.headers.get('Content-Disposition'); + let filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`; + + console.log('📁 다운로드 파일명 처리:', { + original_name: file.files[attachmentIndex].original_name, + content_disposition: contentDisposition, + default_filename: filename + }); + + if (contentDisposition) { + // RFC 5987 filename* 파라미터를 우선 처리 (UTF-8 지원) + const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/); + if (filenameStarMatch) { + filename = decodeURIComponent(filenameStarMatch[1]); + console.log('📁 UTF-8 파일명 추출:', filename); + } else { + // 일반 filename 파라미터 처리 + const filenameMatch = contentDisposition.match(/filename="?([^";\r\n]+)"?/); + if (filenameMatch) { + filename = filenameMatch[1]; + console.log('📁 기본 파일명 추출:', filename); + } + } + } + + // 파일명이 여전히 비어있다면 기본값 사용 + if (!filename || filename.trim() === '') { + filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`; + console.log('📁 기본 파일명 사용:', filename); + } + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + console.log('다운로드 완료'); + this.hideLoadingState(); + + } catch (error) { + console.error('downloadSingleFile 오류:', error); + this.hideLoadingState(); + throw error; + } + } + + handleFileSelection(files) { + const selectedFiles = document.getElementById('selectedFiles'); + if (!selectedFiles) return; + + selectedFiles.innerHTML = ''; + + Array.from(files).forEach(file => { + const fileItem = document.createElement('div'); + fileItem.className = 'selected-file-item'; + fileItem.innerHTML = ` + ${this.getFileIcon(file.name)} + ${this.escapeHtml(file.name)} + ${this.formatFileSize(file.size)} + `; + selectedFiles.appendChild(fileItem); + }); } getFileIcon(fileName) { @@ -936,212 +1162,41 @@ class FileManager { return iconMap[ext] || '📄'; } - removeFile(index) { - if (this.selectedFiles) { - this.selectedFiles.splice(index, 1); - this.updateFilePreview(); - - // 파일 input 업데이트 - const dt = new DataTransfer(); - this.selectedFiles.forEach(file => dt.items.add(file)); - document.getElementById('fileUpload').files = dt.files; - } + formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } - createFilesList() { - const fileGroup = document.querySelector('#fileUpload').closest('.form-group'); - const filesList = document.createElement('div'); - filesList.className = 'files-list'; - fileGroup.appendChild(filesList); - return filesList; + resetForm() { + document.getElementById('fileTitle').value = ''; + document.getElementById('fileDescription').value = ''; + document.getElementById('fileCategory').value = ''; + document.getElementById('fileTags').value = ''; + document.getElementById('fileUpload').value = ''; + + const selectedFiles = document.getElementById('selectedFiles'); + if (selectedFiles) selectedFiles.innerHTML = ''; } - removeFileFromInput(index) { - const fileInput = document.getElementById('fileUpload'); - const dt = new DataTransfer(); - const files = Array.from(fileInput.files); - - files.forEach((file, i) => { - if (i !== index) { - dt.items.add(file); - } - }); - - fileInput.files = dt.files; - this.handleFileUpload({ target: fileInput }); - } - - renderFiles() { - const tbody = document.getElementById('fileList'); - const sortBy = document.getElementById('sortBy').value; - - // 검색 및 필터 적용 (메인 페이지와 동일하게) - const searchTerm = document.getElementById('searchInput') ? document.getElementById('searchInput').value.toLowerCase().trim() : ''; - const categoryFilter = document.getElementById('categoryFilter') ? document.getElementById('categoryFilter').value : ''; - - let filteredFiles = [...this.files]; - - if (searchTerm) { - filteredFiles = filteredFiles.filter(file => - file.title.toLowerCase().includes(searchTerm) || - file.description.toLowerCase().includes(searchTerm) || - (file.tags && file.tags.some(tag => tag.toLowerCase().includes(searchTerm))) - ); - } - - if (categoryFilter) { - filteredFiles = filteredFiles.filter(file => file.category === categoryFilter); - } - - // 정렬 - const sortedFiles = filteredFiles.sort((a, b) => { - switch (sortBy) { - case 'title': - return a.title.localeCompare(b.title); - case 'category': - return a.category.localeCompare(b.category); - case 'date': - default: - return new Date(b.created_at || b.createdAt) - new Date(a.created_at || a.createdAt); - } - }); - - this.allFiles = sortedFiles; - this.updatePagination(); - - if (sortedFiles.length === 0) { - tbody.innerHTML = ` - - 📂 조건에 맞는 자료가 없습니다. - - `; - return; - } - - // 페이지네이션 적용 - const itemsPerPage = 10; - const currentPage = this.currentPage || 1; - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedFiles = sortedFiles.slice(startIndex, endIndex); - - tbody.innerHTML = paginatedFiles.map((file, index) => - this.createFileRowHTML(file, startIndex + index + 1) - ).join(''); - } - - createFileRowHTML(file, rowNumber) { - const createdDate = new Date(file.created_at || file.createdAt).toLocaleDateString('ko-KR'); - const hasAttachments = file.files && file.files.length > 0; - - return ` - - ${rowNumber} - - ${file.category} - - -
- ${this.escapeHtml(file.title)} - ${file.description ? `
${this.escapeHtml(file.description.substring(0, 80))}${file.description.length > 80 ? '...' : ''}` : ''} - ${file.tags && file.tags.length > 0 ? - `
${file.tags.map(tag => `#${this.escapeHtml(tag)}`).join('')}
` : '' - } -
- - - ${hasAttachments ? - `
${file.files.map((f, index) => - `
- ${this.getFileIcon(f.name || f.original_name || 'unknown')} -
-
${this.escapeHtml(f.name || f.original_name || '파일')}
-
${this.formatFileSize(f.size || 0)}
-
-
` - ).join('')}
` : - `-` - } - - ${createdDate} - -
- - - ${hasAttachments ? - `` : '' - } -
- - - `; - } - - createFileHTML(file) { - const createdDate = new Date(file.created_at || file.createdAt).toLocaleDateString('ko-KR'); - const updatedDate = new Date(file.updated_at || file.updatedAt).toLocaleDateString('ko-KR'); - const tagsHTML = file.tags.map(tag => `${tag}`).join(''); - const filesHTML = file.files.length > 0 ? - `
- 첨부파일 (${file.files.length}개): - ${file.files.map(f => `${this.getFileIcon(f.name || f.original_name || 'unknown')} ${f.name || f.original_name || '파일'}`).join(', ')} -
` : ''; - - return ` -
-
-
-
${this.escapeHtml(file.title)}
-
- ${file.category} - 📅 생성: ${createdDate} - ${createdDate !== updatedDate ? `✏️ 수정: ${updatedDate}` : ''} -
-
-
- - ${file.description ? `
${this.escapeHtml(file.description)}
` : ''} - - ${file.tags.length > 0 ? `
${tagsHTML}
` : ''} - - ${filesHTML} - -
- ${!file.isReadOnly && this.currentUser ? ` - - - ` : ''} - ${file.files.length > 0 ? `` : ''} - ${file.isReadOnly ? `👁️ 읽기 전용` : ''} -
-
- `; - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - // 페이지네이션 관련 함수들 updatePagination() { - const totalPages = Math.max(1, Math.ceil(this.allFiles.length / 10)); + const totalPages = Math.max(1, Math.ceil(this.filteredFiles.length / this.itemsPerPage)); const pagination = document.getElementById('pagination'); const prevBtn = document.getElementById('prevPage'); const nextBtn = document.getElementById('nextPage'); const pageInfo = document.getElementById('pageInfo'); - // 항상 페이지네이션을 표시 - if (pagination) pagination.style.display = 'flex'; + if (!pagination) return; + + pagination.style.display = 'flex'; - // 페이지 버튼 상태 업데이트 if (prevBtn) prevBtn.disabled = this.currentPage <= 1; - if (nextBtn) nextBtn.disabled = this.currentPage >= totalPages || this.allFiles.length === 0; + if (nextBtn) nextBtn.disabled = this.currentPage >= totalPages || this.filteredFiles.length === 0; - // 페이지 정보 표시 (아이템이 없어도 1/1로 표시) - const displayTotalPages = this.allFiles.length === 0 ? 1 : totalPages; - const displayCurrentPage = this.allFiles.length === 0 ? 1 : this.currentPage; + const displayTotalPages = this.filteredFiles.length === 0 ? 1 : totalPages; + const displayCurrentPage = this.filteredFiles.length === 0 ? 1 : this.currentPage; if (pageInfo) pageInfo.textContent = `${displayCurrentPage} / ${displayTotalPages}`; } @@ -1149,468 +1204,355 @@ class FileManager { if (this.currentPage > 1) { this.currentPage--; this.renderFiles(); + this.updatePagination(); } } goToNextPage() { - const totalFiles = this.allFiles.length; - const itemsPerPage = 10; - const totalPages = Math.ceil(totalFiles / itemsPerPage); - + const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage); if (this.currentPage < totalPages) { this.currentPage++; this.renderFiles(); - } - } - - // 파일 상세보기 (제목 클릭 시) - viewFile(id) { - const file = this.files.find(f => f.id === id); - if (!file) return; - - this.showDetailView(file); - } - - showDetailView(file) { - // 메인 컨테이너 숨기기 - const container = document.querySelector('.container'); - container.style.display = 'none'; - - // 상세보기 컨테이너 생성 - const detailContainer = document.createElement('div'); - detailContainer.className = 'detail-container'; - detailContainer.id = 'detailContainer'; - - const createdDate = new Date(file.created_at || file.createdAt).toLocaleDateString('ko-KR'); - const updatedDate = new Date(file.updated_at || file.updatedAt).toLocaleDateString('ko-KR'); - - detailContainer.innerHTML = ` -
-
-

📋 자료 상세보기

-

등록된 자료의 상세 정보를 확인하고 관리하세요

-
- -
-
-

📄 ${this.escapeHtml(file.title)}

-
- - - -
-
- -
-
-
- -
- ${file.category} -
-
- -
- -
- ${file.description ? this.escapeHtml(file.description) : '설명이 없습니다.'} -
-
- - ${file.tags && file.tags.length > 0 ? ` -
- -
-
- ${file.tags.map(tag => `#${this.escapeHtml(tag)}`).join('')} -
-
-
` : ''} - - ${file.files && file.files.length > 0 ? ` -
- -
-
- ${file.files.map((f, index) => ` -
- ${this.getFileIcon(f.name || f.original_name || 'unknown')} - ${this.escapeHtml(f.name || f.original_name || '파일')} - -
- `).join('')} -
-
- -
-
-
` : ` -
- -
- 첨부된 파일이 없습니다. -
-
`} - -
- -
${createdDate}
-
- - ${createdDate !== updatedDate ? ` -
- -
${updatedDate}
-
` : ''} -
-
-
-
- `; - - document.body.appendChild(detailContainer); - } - - hideDetailView() { - const detailContainer = document.getElementById('detailContainer'); - if (detailContainer) { - detailContainer.remove(); - } - - // 메인 컨테이너 다시 보이기 - const container = document.querySelector('.container'); - container.style.display = 'block'; - } - - editFileFromDetail(id) { - this.hideDetailView(); - this.editFile(id); - } - - async deleteFileFromDetail(id) { - if (confirm('정말로 이 자료를 삭제하시겠습니까?')) { - await this.deleteFile(id); - this.hideDetailView(); - } - } - - editFile(id) { - if (!this.currentUser) { - this.showNotification('로그인이 필요합니다.', 'error'); - return; - } - - const file = this.files.find(f => f.id === id); - if (!file) return; - - if (file.isReadOnly) { - this.showNotification('읽기 전용 파일은 편집할 수 없습니다.', 'error'); - return; - } - - this.currentEditId = id; - - document.getElementById('editTitle').value = file.title; - document.getElementById('editDescription').value = file.description; - document.getElementById('editCategory').value = file.category; - document.getElementById('editTags').value = file.tags.join(', '); - - document.getElementById('editModal').style.display = 'block'; - } - - handleEditSubmit(e) { - e.preventDefault(); - - if (!this.currentEditId) return; - - const title = document.getElementById('editTitle').value.trim(); - const description = document.getElementById('editDescription').value.trim(); - const category = document.getElementById('editCategory').value; - const tags = document.getElementById('editTags').value.split(',').map(tag => tag.trim()).filter(tag => tag); - - if (!title || !category) { - alert('제목과 카테고리는 필수 입력 항목입니다.'); - return; - } - - const updates = { - title, - description, - category, - tags - }; - - this.updateFileInSupabase(this.currentEditId, updates); - this.closeEditModal(); - } - - closeEditModal() { - document.getElementById('editModal').style.display = 'none'; - this.currentEditId = null; - } - - deleteFile(id) { - if (!this.currentUser) { - this.showNotification('로그인이 필요합니다.', 'error'); - return; - } - - const file = this.files.find(f => f.id === id); - if (!file) return; - - if (file.isReadOnly) { - this.showNotification('읽기 전용 파일은 삭제할 수 없습니다.', 'error'); - return; - } - - if (confirm(`"${file.title}" 자료를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) { - this.deleteFileFromSupabase(id); - } - } - - async downloadFiles(id) { - const file = this.files.find(f => f.id === id); - if (!file || file.files.length === 0) return; - - try { - if (file.files.length === 1) { - // 단일 파일: 직접 다운로드 - await this.downloadSingleFileData(file.files[0]); - const fileName = file.files[0].original_name || file.files[0].name; - this.showNotification(`파일 다운로드 완료: ${fileName}`, 'success'); - } else { - // 다중 파일: ZIP으로 압축하여 다운로드 - await this.downloadFilesAsZip(file); - } - } catch (error) { - console.error('파일 다운로드 오류:', error); - this.showNotification('파일 다운로드 중 오류가 발생했습니다.', 'error'); - } - } - - async downloadSingleFile(fileId, fileIndex) { - const file = this.files.find(f => f.id === fileId); - if (!file || !file.files[fileIndex]) return; - - try { - const fileData = file.files[fileIndex]; - await this.downloadSingleFileData(fileData); - const fileName = fileData.original_name || fileData.name; - this.showNotification(`파일 다운로드 완료: ${fileName}`, 'success'); - } catch (error) { - console.error('개별 파일 다운로드 오류:', error); - this.showNotification('파일 다운로드 중 오류가 발생했습니다.', 'error'); - } - } - - async downloadSingleFileData(fileData) { - if (fileData.storage_path && isSupabaseConfigured()) { - // Supabase Storage에서 다운로드 - await this.downloadFileFromStorage(fileData.storage_path, fileData.original_name || fileData.name); - } else if (fileData.data) { - // localStorage 데이터 다운로드 (base64) - const link = document.createElement('a'); - link.href = fileData.data; - link.download = fileData.name || fileData.original_name; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - } - - async downloadFilesAsZip(file) { - const zip = new JSZip(); - const fileTitle = file.title.replace(/[<>:"/\\|?*]/g, '_'); // 파일명에 부적절한 문자 제거 - - for (const fileData of file.files) { - try { - let fileContent; - const fileName = fileData.original_name || fileData.name || 'file'; - - if (fileData.storage_path && isSupabaseConfigured()) { - // Supabase Storage에서 파일 가져오기 - const response = await fetch(await SupabaseHelper.getFileUrl(fileData.storage_path)); - fileContent = await response.blob(); - } else if (fileData.data) { - // localStorage의 base64 데이터 변환 - const response = await fetch(fileData.data); - fileContent = await response.blob(); - } - - if (fileContent) { - zip.file(fileName, fileContent); - } - } catch (error) { - console.error(`파일 ${fileData.name} 처리 오류:`, error); - } - } - - // ZIP 파일 생성 및 다운로드 - const zipBlob = await zip.generateAsync({ type: "blob" }); - const link = document.createElement('a'); - link.href = URL.createObjectURL(zipBlob); - link.download = `${fileTitle}_첨부파일.zip`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(link.href); - - this.showNotification(`ZIP 파일 다운로드 완료: ${fileTitle}_첨부파일.zip (${file.files.length}개 파일)`, 'success'); - } - - handleSearch() { - const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim(); - const categoryFilter = document.getElementById('categoryFilter').value; - - // 검색 시 첫 페이지로 리셋 - this.currentPage = 1; - this.renderFiles(); - } - - - clearForm() { - document.getElementById('fileForm').reset(); - const filesList = document.querySelector('.files-list'); - if (filesList) { - filesList.innerHTML = ''; - } - // 선택된 파일들과 미리보기 초기화 - this.selectedFiles = []; - const selectedFilesContainer = document.getElementById('selectedFiles'); - if (selectedFilesContainer) { - selectedFilesContainer.innerHTML = ''; - } - } - - formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } - - updateEmptyState() { - const container = document.getElementById('fileList'); - if (this.files.length === 0) { - container.innerHTML = '

📂 등록된 자료가 없습니다. 새 자료를 추가해보세요!

'; + this.updatePagination(); } } showNotification(message, type = 'info') { - // 기존 알림이 있으면 제거 - const existingNotification = document.querySelector('.notification'); - if (existingNotification) { - existingNotification.remove(); - } - const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + color: white; + font-weight: 500; + z-index: 10000; + max-width: 400px; + background: ${type === 'success' ? '#48bb78' : type === 'error' ? '#f56565' : '#4299e1'}; + `; document.body.appendChild(notification); - // 3초 후 자동 제거 setTimeout(() => { - if (notification.parentNode) { - notification.remove(); + if (document.body.contains(notification)) { + document.body.removeChild(notification); } }, 3000); } - loadFiles() { + showFileDetail(id) { + const file = this.files.find(f => f.id === id); + if (!file) { + this.showNotification('파일을 찾을 수 없습니다.', 'error'); + return; + } + + const tags = this.parseJsonTags(file.tags); + const createdDate = new Date(file.created_at).toLocaleString('ko-KR'); + const updatedDate = new Date(file.updated_at).toLocaleString('ko-KR'); + + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + } + + async handleAddCategory() { + const categoryName = document.getElementById('categoryName').value.trim(); + + if (!categoryName) { + this.showNotification('카테고리 이름을 입력해주세요.', 'error'); + return; + } + + // 중복 확인 + if (this.categories.some(cat => cat.name === categoryName)) { + this.showNotification('이미 존재하는 카테고리입니다.', 'error'); + return; + } + try { - const stored = localStorage.getItem('fileManagerData'); - const files = stored ? JSON.parse(stored) : []; - - // 기존 localStorage 데이터의 호환성을 위해 컬럼명 변환 - return files.map(file => ({ - ...file, - created_at: file.created_at || file.createdAt || new Date().toISOString(), - updated_at: file.updated_at || file.updatedAt || new Date().toISOString() - })); + const data = await window.AdminAPI.Categories.create(categoryName); + this.showNotification('카테고리가 추가되었습니다!', 'success'); + this.resetCategoryForm(); + await this.loadData(); + this.renderCategoryList(); } catch (error) { - console.error('파일 데이터를 불러오는 중 오류가 발생했습니다:', error); - return []; + console.error('카테고리 추가 오류:', error); + this.showNotification(error.message || '카테고리 추가 중 오류가 발생했습니다.', 'error'); } } - saveFiles() { + resetCategoryForm() { + document.getElementById('categoryName').value = ''; + } + + renderCategoryList() { + console.log('renderCategoryList 호출됨'); + console.log('현재 카테고리 데이터:', this.categories); + + const categoryList = document.getElementById('categoryList'); + if (!categoryList) { + console.error('categoryList 요소를 찾을 수 없습니다'); + return; + } + + if (this.categories.length === 0) { + console.log('카테고리가 없어서 빈 메시지 표시'); + categoryList.innerHTML = '

등록된 카테고리가 없습니다.

'; + return; + } + + console.log('카테고리 목록 렌더링 시작'); + categoryList.innerHTML = this.categories.map(category => { + console.log('카테고리 렌더링:', category); + return ` +
+ ${this.escapeHtml(category.name)} +
+ + +
+
+ `; + }).join(''); + console.log('카테고리 목록 렌더링 완료'); + } + + editCategory(id) { + console.log('editCategory 호출됨, ID:', id, 'Type:', typeof id); + console.log('전체 카테고리 목록:', this.categories); + + const category = this.categories.find(c => { + console.log('비교:', c.id, 'vs', id, 'Type:', typeof c.id, 'vs', typeof id); + return c.id == id; // == 사용으로 타입 변환 허용 + }); + + console.log('찾은 카테고리:', category); + + if (!category) { + console.error('카테고리를 찾을 수 없습니다. ID:', id); + this.showNotification('카테고리를 찾을 수 없습니다.', 'error'); + return; + } + + document.getElementById('editCategoryName').value = category.name; + this.currentEditCategoryId = id; + document.getElementById('editCategoryModal').style.display = 'flex'; + } + + async handleUpdateCategory() { + if (!this.currentEditCategoryId) { + this.showNotification('수정할 카테고리를 찾을 수 없습니다.', 'error'); + return; + } + + const categoryName = document.getElementById('editCategoryName').value.trim(); + + if (!categoryName) { + this.showNotification('카테고리 이름을 입력해주세요.', 'error'); + return; + } + + // 중복 확인 (자기 자신 제외) + if (this.categories.some(cat => cat.name === categoryName && cat.id !== this.currentEditCategoryId)) { + this.showNotification('이미 존재하는 카테고리입니다.', 'error'); + return; + } + try { - localStorage.setItem('fileManagerData', JSON.stringify(this.files)); + const data = await window.AdminAPI.Categories.update(this.currentEditCategoryId, categoryName); + this.showNotification('카테고리가 수정되었습니다!', 'success'); + document.getElementById('editCategoryModal').style.display = 'none'; + this.currentEditCategoryId = null; + await this.loadData(); + this.renderCategoryList(); } catch (error) { - console.error('파일 데이터를 저장하는 중 오류가 발생했습니다:', error); - alert('데이터 저장 중 오류가 발생했습니다. 브라우저의 저장공간을 확인해주세요.'); + console.error('카테고리 수정 오류:', error); + this.showNotification(error.message || '카테고리 수정 중 오류가 발생했습니다.', 'error'); } } - exportData() { - const dataStr = JSON.stringify(this.files, null, 2); - const dataBlob = new Blob([dataStr], {type: 'application/json'}); - const url = URL.createObjectURL(dataBlob); - const link = document.createElement('a'); - link.href = url; - link.download = `자료실_백업_${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - this.showNotification('데이터가 성공적으로 내보내기되었습니다!', 'success'); - } + async deleteCategory(id) { + console.log('deleteCategory 호출됨, ID:', id, 'Type:', typeof id); + console.log('전체 카테고리 목록:', this.categories); + + const category = this.categories.find(c => { + console.log('비교:', c.id, 'vs', id, 'Type:', typeof c.id, 'vs', typeof id); + return c.id == id; // == 사용으로 타입 변환 허용 + }); + + console.log('찾은 카테고리:', category); + + if (!category) { + console.error('카테고리를 찾을 수 없습니다. ID:', id); + this.showNotification('카테고리를 찾을 수 없습니다.', 'error'); + return; + } - importData(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - try { - const importedData = JSON.parse(e.target.result); - if (Array.isArray(importedData)) { - if (confirm('기존 데이터를 모두 삭제하고 새 데이터를 가져오시겠습니까?')) { - this.files = importedData; - this.saveFiles(); - this.renderFiles(); - this.updateEmptyState(); - this.showNotification('데이터가 성공적으로 가져와졌습니다!', 'success'); - } - } else { - alert('올바르지 않은 파일 형식입니다.'); - } - } catch (error) { - alert('파일을 읽는 중 오류가 발생했습니다.'); - console.error(error); + // 해당 카테고리를 사용하는 파일이 있는지 확인 + const filesUsingCategory = this.files.filter(file => file.category === category.name); + if (filesUsingCategory.length > 0) { + if (!confirm(`이 카테고리는 ${filesUsingCategory.length}개의 파일에서 사용 중입니다.\n정말로 삭제하시겠습니까? (사용 중인 파일들의 카테고리가 '기타'로 변경됩니다.)`)) { + return; } - }; - reader.readAsText(file); + } else { + if (!confirm(`정말로 '${category.name}' 카테고리를 삭제하시겠습니까?`)) { + return; + } + } + + try { + await window.AdminAPI.Categories.delete(id); + this.showNotification('카테고리가 삭제되었습니다.', 'success'); + await this.loadData(); + this.renderCategoryList(); + } catch (error) { + console.error('카테고리 삭제 오류:', error); + this.showNotification(error.message || '카테고리 삭제 중 오류가 발생했습니다.', 'error'); + } + } + + // 로딩 상태 표시 + showLoadingState(message = '처리 중...', disableForm = false) { + console.log('🔄 로딩 상태 표시:', message); + + // 기존 로딩 인디케이터 제거 + this.hideLoadingState(); + + // 로딩 오버레이 생성 + const loadingOverlay = document.createElement('div'); + loadingOverlay.id = 'loadingOverlay'; + loadingOverlay.className = 'loading-overlay'; + loadingOverlay.innerHTML = ` +
+
+
${message}
+
+ `; + + document.body.appendChild(loadingOverlay); + + // 폼 비활성화 (선택적) + if (disableForm) { + const forms = document.querySelectorAll('form, button'); + forms.forEach(element => { + element.style.pointerEvents = 'none'; + element.style.opacity = '0.6'; + }); + } + } + + // 로딩 상태 숨김 + hideLoadingState() { + console.log('✅ 로딩 상태 해제'); + + // 로딩 오버레이 제거 + const loadingOverlay = document.getElementById('loadingOverlay'); + if (loadingOverlay) { + loadingOverlay.remove(); + } + + // 폼 활성화 + const forms = document.querySelectorAll('form, button'); + forms.forEach(element => { + element.style.pointerEvents = ''; + element.style.opacity = ''; + }); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } } -const style = document.createElement('style'); -style.textContent = ` - @keyframes slideIn { - from { transform: translateX(100%); opacity: 0; } - to { transform: translateX(0); opacity: 1; } - } - @keyframes slideOut { - from { transform: translateX(0); opacity: 1; } - to { transform: translateX(100%); opacity: 0; } - } -`; -document.head.appendChild(style); - -const fileManager = new FileManager(); - +// 전역 인스턴스 생성 +let adminManager; document.addEventListener('DOMContentLoaded', () => { - console.log('📚 자료실 관리 시스템이 초기화되었습니다.'); + adminManager = new AdminFileManager(); + window.adminManager = adminManager; // 전역 접근 가능하도록 }); \ No newline at end of file diff --git a/admin/styles.css b/admin/styles.css index 724e5a4..82d8816 100644 --- a/admin/styles.css +++ b/admin/styles.css @@ -39,6 +39,149 @@ header p { font-size: 1.1rem; } +/* 관리자 로그인 스타일 */ +.login-section { + margin-top: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.login-form { + background: rgba(255, 255, 255, 0.9); + padding: 30px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; +} + +.login-form h3 { + text-align: center; + margin-bottom: 20px; + color: #4a5568; + font-size: 1.4rem; +} + +.login-form .form-group { + margin-bottom: 15px; +} + +.login-form input { + width: 100%; + padding: 12px 15px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1rem; + transition: all 0.3s ease; +} + +.login-form input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.login-btn { + width: 100%; + padding: 12px 20px; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.login-btn:hover { + background: #5a67d8; + transform: translateY(-1px); +} + +.login-btn:disabled { + background: #cbd5e0; + cursor: not-allowed; + transform: none; +} + +.admin-section { + margin-top: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.admin-info { + display: flex; + align-items: center; + gap: 15px; + background: rgba(72, 187, 120, 0.1); + padding: 12px 20px; + border-radius: 8px; + border: 1px solid rgba(72, 187, 120, 0.3); +} + +.admin-info span { + color: #2f855a; + font-weight: 500; +} + +.connection-status { + display: flex; + align-items: center; + gap: 5px; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 500; +} + +.connection-status.online { + background: rgba(72, 187, 120, 0.1); + color: #2f855a; +} + +.logout-btn { + padding: 6px 12px; + background: #e53e3e; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.logout-btn:hover { + background: #c53030; +} + +.public-link { + margin-top: 10px; +} + +.public-btn { + display: inline-block; + padding: 8px 16px; + background: #48bb78; + color: white; + text-decoration: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.public-btn:hover { + background: #38a169; + transform: translateY(-1px); +} + .auth-section { margin-top: 20px; display: flex; @@ -131,6 +274,54 @@ header p { box-shadow: 0 2px 10px rgba(0, 184, 148, 0.2); } +/* 데이터베이스 모드 스타일 */ +.database-mode { + background: linear-gradient(135deg, #81ecec, #74b9ff); + color: #2d3436; + padding: 15px 20px; + border-radius: 8px; + margin: 20px 0; + border: 2px solid #0984e3; + box-shadow: 0 2px 10px rgba(9, 132, 227, 0.2); +} + +.database-mode-content { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +.database-info { + font-weight: 500; + color: #0984e3; +} + +/* 로그인 필요 모드 스타일 */ +.auth-required-mode { + background: linear-gradient(135deg, #fdcb6e, #e17055); + color: #2d3436; + padding: 15px 20px; + border-radius: 8px; + margin: 20px 0; + border: 2px solid #e17055; + box-shadow: 0 2px 10px rgba(225, 112, 85, 0.2); +} + +.auth-required-content { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +.auth-info { + font-weight: 500; + color: #d63031; +} + .guest-mode-content { display: flex; justify-content: space-between; @@ -256,6 +447,150 @@ header p { font-size: 1.8rem; } +/* 탭 스타일 */ +.section-tabs { + display: flex; + gap: 10px; + margin-bottom: 25px; + border-bottom: 2px solid #e2e8f0; +} + +.tab-btn { + padding: 12px 24px; + border: none; + background: transparent; + color: #6b7280; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.3s ease; +} + +.tab-btn:hover { + color: #667eea; + background: rgba(102, 126, 234, 0.1); +} + +.tab-btn.active { + color: #667eea; + border-bottom-color: #667eea; + background: rgba(102, 126, 234, 0.1); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* 카테고리 관리 스타일 */ +.category-list-section { + margin-top: 40px; + padding-top: 30px; + border-top: 2px solid #e2e8f0; +} + +.category-list-section h3 { + color: #4a5568; + margin-bottom: 20px; + font-size: 1.4rem; +} + +.category-list { + display: grid; + gap: 10px; +} + +.category-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 8px; + transition: all 0.3s ease; +} + +.category-item:hover { + border-color: #667eea; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); +} + +.category-info { + display: flex; + align-items: center; + gap: 15px; + flex: 1; +} + +.category-name { + font-weight: 600; + color: #374151; + font-size: 1.1rem; +} + +.category-count { + background: #667eea; + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 500; +} + +.category-actions { + display: flex; + gap: 8px; +} + +.category-btn { + padding: 6px 12px; + border: none; + border-radius: 4px; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; +} + +.category-edit-btn { + background: #3b82f6; + color: white; +} + +.category-edit-btn:hover { + background: #2563eb; + transform: translateY(-1px); +} + +.category-delete-btn { + background: #ef4444; + color: white; +} + +.category-delete-btn:hover { + background: #dc2626; + transform: translateY(-1px); +} + +.category-default { + opacity: 0.7; +} + +.category-default .category-delete-btn { + background: #9ca3af; + cursor: not-allowed; +} + +.category-default .category-delete-btn:hover { + background: #9ca3af; + transform: none; +} + .form-group { margin-bottom: 20px; } @@ -539,6 +874,272 @@ header p { color: #9ca3af; } +/* 첨부파일 관리 스타일 */ +.attachment-management { + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 20px; + background: #f9fafb; + gap: 20px; + display: flex; + flex-direction: column; +} + +/* 섹션 제목 스타일 */ +.section-title { + font-size: 1rem; + font-weight: 600; + color: #374151; + margin: 0 0 12px 0; + padding-bottom: 8px; + border-bottom: 2px solid #e5e7eb; +} + +/* 기존 첨부파일 섹션 */ +.existing-attachments-section { + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; +} + +.existing-attachments { + margin-top: 12px; +} + +.no-existing-files { + text-align: center; + padding: 20px; + color: #6b7280; + font-style: italic; +} + +.placeholder-text { + font-size: 0.9rem; +} + +/* 새 파일 추가 섹션 */ +.new-attachments-section { + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; +} + +/* 드래그&드롭 영역 */ +.file-drop-zone { + border: 2px dashed #d1d5db; + border-radius: 8px; + padding: 30px 20px; + text-align: center; + background: #f8fafc; + transition: all 0.3s ease; + cursor: pointer; + margin-top: 12px; +} + +.file-drop-zone:hover { + border-color: #3b82f6; + background: #eff6ff; +} + +.file-drop-zone.dragover { + border-color: #3b82f6; + background: #dbeafe; + transform: scale(1.02); +} + +.drop-zone-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.drop-zone-icon { + font-size: 3rem; + opacity: 0.6; +} + +.drop-zone-text p { + margin: 4px 0; + color: #374151; +} + +.or-text { + color: #6b7280; + font-size: 0.9rem; +} + +.file-select-btn { + background: #3b82f6; + color: white; + border: none; + padding: 12px 24px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.file-select-btn:hover { + background: #2563eb; + transform: translateY(-1px); +} + +.drop-zone-hint { + margin-top: 8px; + color: #6b7280; +} + +/* 새 파일 미리보기 */ +.new-files-preview { + margin-top: 16px; + display: none; +} + +.new-files-preview.show { + display: block; +} + +.preview-file-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 6px; + margin-bottom: 8px; + transition: all 0.2s ease; +} + +.preview-file-info { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} + +.preview-file-icon { + font-size: 1.2rem; + flex-shrink: 0; +} + +.preview-file-details { + flex: 1; + min-width: 0; +} + +.preview-file-name { + font-weight: 500; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.preview-file-size { + color: #64748b; + font-size: 0.85rem; +} + +.preview-file-remove { + background: #ef4444; + color: white; + border: none; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 0.8rem; + transition: all 0.2s ease; +} + +.preview-file-remove:hover { + background: #dc2626; +} + +.attachment-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 6px; + margin-bottom: 8px; + transition: all 0.2s ease; +} + +.attachment-item:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-color: #3b82f6; +} + +.attachment-info { + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.attachment-icon { + font-size: 1.2rem; +} + +.attachment-details { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; +} + +.attachment-name { + font-weight: 500; + color: #1f2937; + font-size: 0.9rem; +} + +.attachment-size { + color: #6b7280; + font-size: 0.75rem; +} + +.attachment-actions { + display: flex; + gap: 5px; +} + +.attachment-delete-btn { + padding: 4px 8px; + background: #ef4444; + color: white; + border: none; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.attachment-delete-btn:hover { + background: #dc2626; + transform: scale(1.05); +} + +/* 기존 new-attachment-section 및 add-btn 스타일 제거됨 - 새로운 구조로 대체 */ + +.attachment-preview { + display: none; + margin-top: 10px; + padding: 10px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 4px; + font-size: 0.85rem; + color: #4b5563; +} + /* 액션 버튼 */ .action-buttons { display: flex; @@ -791,6 +1392,96 @@ header p { text-align: center; } +.attachment-info { + display: flex; + align-items: center; + gap: 5px; + justify-content: center; +} + +.attachment-info .attachment-icon { + font-size: 1.2rem; + min-width: auto; +} + +.attachment-count { + font-size: 0.85rem; + color: #6b7280; +} + +/* 제목 링크 스타일 */ +.title-link { + color: #2563eb; + text-decoration: none; + font-weight: 600; +} + +.title-link:hover { + color: #1d4ed8; + text-decoration: underline; +} + +/* 관리자 페이지 첨부파일 목록 */ +.attachment-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 120px; + overflow-y: auto; +} + +.attachment-item-admin { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + background: #f8fafc; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid #e2e8f0; +} + +.attachment-item-admin:hover { + background: #e2e8f0; + border-color: #cbd5e1; + transform: translateY(-1px); +} + +.attachment-file-icon { + font-size: 0.9rem; + min-width: 16px; + text-align: center; +} + +.attachment-file-name { + flex: 1; + font-size: 0.75rem; + color: #475569; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 120px; +} + +/* 모달 태그 스타일 */ +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 4px; +} + +.tag-item { + display: inline-block; + background: #e5e7eb; + color: #374151; + padding: 4px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; +} + .attachment-name { flex: 1; font-weight: 500; @@ -1145,7 +1836,7 @@ header p { .modal { display: none; position: fixed; - z-index: 1000; + z-index: 10000; left: 0; top: 0; width: 100%; @@ -1154,6 +1845,10 @@ header p { backdrop-filter: blur(5px); } +#editCategoryModal { + z-index: 10001; +} + .modal-content { background: white; margin: 5% auto; @@ -1364,4 +2059,605 @@ header p { opacity: 0; transform: translateX(100%); } +} + +/* 페이지네이션 스타일 */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-top: 20px; + padding: 20px 0; +} + +.page-btn { + padding: 10px 16px; + border: 2px solid #667eea; + background: white; + color: #667eea; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.page-btn:hover:not(:disabled) { + background: #667eea; + color: white; + transform: translateY(-1px); +} + +.page-btn:disabled { + background: #f3f4f6; + color: #9ca3af; + border-color: #d1d5db; + cursor: not-allowed; + transform: none; +} + +#pageInfo { + padding: 8px 12px; + background: #f8fafc; + border-radius: 6px; + font-weight: 500; + color: #4a5568; + min-width: 60px; + text-align: center; +} + +/* 테이블 반응형 스타일 강화 */ +@media (max-width: 1024px) { + .board-table { + font-size: 0.85rem; + } + + .col-no { width: 50px; } + .col-category { width: 80px; } + .col-attachment { width: 180px; } + .col-date { width: 100px; } + .col-actions { width: 120px; } + + .action-btn { + padding: 3px 6px; + font-size: 0.75rem; + } + + .attachment-file-name { + font-size: 0.75rem; + } + + .attachment-file-size { + font-size: 0.65rem; + } +} + +@media (max-width: 768px) { + .board-container { + padding: 10px; + overflow-x: auto; + } + + .board-table { + min-width: 700px; + font-size: 0.8rem; + } + + .board-table th, + .board-table td { + padding: 6px 8px; + } + + .col-title { + min-width: 180px; + } + + .board-title { + font-size: 0.9rem; + line-height: 1.3; + } + + .category-badge { + font-size: 0.75rem; + padding: 2px 6px; + } + + .attachment-icons { + max-height: 80px; + font-size: 0.75rem; + } + + .action-buttons { + flex-direction: column; + gap: 2px; + } + + .action-btn { + width: 100%; + padding: 2px 4px; + font-size: 0.7rem; + } + + .pagination { + flex-wrap: wrap; + gap: 10px; + } + + .page-btn { + padding: 8px 12px; + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .board-table { + min-width: 600px; + } + + .col-no { display: none; } + .col-date { width: 80px; } + .col-actions { width: 100px; } + + .board-table th:first-child, + .board-table td:first-child { + display: none; + } +} + +/* 모달 오버레이 및 상세보기 모달 스타일 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(5px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-content { + background: white; + border-radius: 15px; + width: 90%; + max-width: 700px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + animation: modalSlideIn 0.3s ease; + position: relative; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + padding: 25px 30px 15px; + border-bottom: 2px solid #f1f5f9; + display: flex; + justify-content: between; + align-items: center; +} + +.modal-header h3 { + margin: 0; + color: #1e293b; + font-size: 1.5rem; + font-weight: 600; +} + +.modal-close { + background: none; + border: none; + font-size: 1.5rem; + color: #64748b; + cursor: pointer; + padding: 5px; + border-radius: 50%; + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; +} + +.modal-close:hover { + background: #f1f5f9; + color: #334155; +} + +.modal-body { + padding: 20px 30px; +} + +.info-group { + margin-bottom: 20px; +} + +.info-group label { + display: block; + font-size: 0.9rem; + font-weight: 600; + color: #374151; + margin-bottom: 8px; +} + +.info-value { + font-size: 0.95rem; + color: #1f2937; + line-height: 1.5; +} + +.modal-footer { + padding: 20px 30px 30px; + border-top: 1px solid #f1f5f9; + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-secondary { + background: #e2e8f0; + color: #475569; +} + +.btn-secondary:hover { + background: #cbd5e1; +} + +.btn-primary { + background: #3b82f6; + color: white; +} + +.btn-primary:hover { + background: #2563eb; +} + +.btn-danger { + background: #ef4444; + color: white; +} + +.btn-danger:hover { + background: #dc2626; +} + +/* 모달 내 첨부파일 스타일 */ +.attachments-list { + display: flex; + flex-direction: column; + gap: 10px; + margin: 10px 0; +} + +.attachment-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: #f8fafc; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.attachment-name { + flex: 1; + font-weight: 500; + color: #374151; + word-break: break-all; +} + +.download-single-btn { + background: #10b981; + color: white; + border: none; + padding: 6px 12px; + border-radius: 6px; + font-size: 0.8rem; + cursor: pointer; + transition: background 0.2s ease; +} + +.download-single-btn:hover { + background: #059669; +} + +.attachment-actions { + margin-top: 15px; + text-align: center; +} + +.download-all-btn { + background: #3b82f6; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease; +} + +.download-all-btn:hover { + background: #2563eb; +} + +/* 탭 스타일 */ +.section-tabs { + display: flex; + gap: 5px; + margin-bottom: 20px; + border-bottom: 2px solid #e2e8f0; +} + +.tab-btn { + background: none; + border: none; + padding: 12px 24px; + font-size: 1rem; + font-weight: 500; + color: #718096; + cursor: pointer; + border-radius: 8px 8px 0 0; + transition: all 0.3s ease; + position: relative; +} + +.tab-btn:hover { + background: rgba(102, 126, 234, 0.1); + color: #667eea; +} + +.tab-btn.active { + color: #667eea; + background: white; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); +} + +.tab-btn.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: #667eea; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* 카테고리 관리 스타일 */ +.category-list-section { + margin-top: 30px; + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.category-list { + display: flex; + flex-direction: column; + gap: 10px; + max-height: 400px; + overflow-y: auto; +} + +.category-item { + display: flex; + justify-content: space-between; + align-items: center; + background: white; + padding: 15px 20px; + border-radius: 8px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.category-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.category-name { + font-weight: 500; + color: #4a5568; + font-size: 1.1rem; +} + +.category-actions { + display: flex; + gap: 8px; +} + +.btn-edit-category, +.btn-delete-category { + background: none; + border: none; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 1.1rem; + transition: all 0.3s ease; +} + +.btn-edit-category { + color: #3182ce; + background: rgba(49, 130, 206, 0.1); +} + +.btn-edit-category:hover { + background: rgba(49, 130, 206, 0.2); + transform: scale(1.1); +} + +.btn-delete-category { + color: #e53e3e; + background: rgba(229, 62, 62, 0.1); +} + +.btn-delete-category:hover { + background: rgba(229, 62, 62, 0.2); + transform: scale(1.1); +} + +.empty-message { + text-align: center; + color: #718096; + font-style: italic; + padding: 40px; + background: rgba(255, 255, 255, 0.7); + border-radius: 8px; +} + +/* 수정 모달 스타일 추가 */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(3px); + align-items: center; + justify-content: center; +} + +.modal h2 { + margin-bottom: 20px; + color: #4a5568; + text-align: center; +} + +/* 반응형 */ +@media (max-width: 768px) { + .section-tabs { + flex-direction: column; + gap: 0; + } + + .tab-btn { + border-radius: 0; + } + + .category-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .category-actions { + align-self: flex-end; + } + .modal-content { + width: 95%; + max-height: 95vh; + } + + .modal-header, .modal-body, .modal-footer { + padding-left: 20px; + padding-right: 20px; + } + + .modal-footer { + flex-direction: column; + } + + .btn { + width: 100%; + } +} + +/* 로딩 오버레이 스타일 */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + backdrop-filter: blur(3px); +} + +.loading-content { + background: white; + padding: 40px 60px; + border-radius: 15px; + text-align: center; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + max-width: 90%; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid #f3f3f3; + border-top: 4px solid #2196F3; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-message { + font-size: 16px; + color: #333; + font-weight: 500; + margin-top: 15px; +} + +/* 로딩 중 폼 비활성화 스타일 */ +.loading-overlay + * form, +.loading-overlay + * button { + pointer-events: none; + opacity: 0.6; + transition: opacity 0.3s ease; } \ No newline at end of file diff --git a/api-client.js b/api-client.js new file mode 100644 index 0000000..fd0ba5e --- /dev/null +++ b/api-client.js @@ -0,0 +1,50 @@ +// 일반 사용자용 API 클라이언트 +// SQLite 백엔드와 통신하는 함수들 + +const API_BASE_URL = ''; + +// API 요청 헬퍼 함수 +async function apiRequest(url, options = {}) { + const response = await fetch(`${API_BASE_URL}${url}`, { + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`API Error: ${response.status} - ${error}`); + } + + return response; +} + +// 공개 파일 목록 조회 +async function getPublicFiles() { + try { + const response = await apiRequest('/api/files/public'); + return await response.json(); + } catch (error) { + console.error('공개 파일 목록 조회 오류:', error); + throw error; + } +} + +// 파일 다운로드 +async function downloadFile(fileId, attachmentId) { + try { + const response = await apiRequest(`/api/download/${fileId}/${attachmentId}`); + return response; + } catch (error) { + console.error('파일 다운로드 오류:', error); + throw error; + } +} + +// 전역으로 내보내기 +window.ApiClient = { + getPublicFiles, + downloadFile +}; \ No newline at end of file diff --git a/admin/supabase-config.js b/backup/supabase/admin-supabase-config.js similarity index 100% rename from admin/supabase-config.js rename to backup/supabase/admin-supabase-config.js diff --git a/clean-storage-setup.sql b/backup/supabase/clean-storage-setup.sql similarity index 100% rename from clean-storage-setup.sql rename to backup/supabase/clean-storage-setup.sql diff --git a/setup-guide.md b/backup/supabase/setup-guide.md similarity index 100% rename from setup-guide.md rename to backup/supabase/setup-guide.md diff --git a/storage-policies.sql b/backup/supabase/storage-policies.sql similarity index 100% rename from storage-policies.sql rename to backup/supabase/storage-policies.sql diff --git a/supabase-config.js b/backup/supabase/supabase-config.js similarity index 100% rename from supabase-config.js rename to backup/supabase/supabase-config.js diff --git a/supabase-schema.sql b/backup/supabase/supabase-schema.sql similarity index 100% rename from supabase-schema.sql rename to backup/supabase/supabase-schema.sql diff --git a/temp-public-policy.sql b/backup/supabase/temp-public-policy.sql similarity index 100% rename from temp-public-policy.sql rename to backup/supabase/temp-public-policy.sql diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..c153b4c --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 1755826306 connect.sid s%3Alkct8oX-9zlTMoD6Mu3BP0RwCz0CFR-X.Mad5GaxzMugYjbnKEOxUq9mnbJkkJY3O79f3q%2BaE%2BJ4 diff --git a/database/db-helper.js b/database/db-helper.js new file mode 100644 index 0000000..b1f9604 --- /dev/null +++ b/database/db-helper.js @@ -0,0 +1,576 @@ +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const fs = require('fs'); + +class DatabaseHelper { + constructor() { + this.dbPath = path.join(__dirname, 'jaryo.db'); + this.db = null; + } + + // 데이터베이스 연결 + connect() { + return new Promise((resolve, reject) => { + if (this.db) { + resolve(this.db); + return; + } + + this.db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READWRITE, (err) => { + if (err) { + console.error('데이터베이스 연결 오류:', err.message); + reject(err); + } else { + console.log('✅ SQLite 데이터베이스 연결됨'); + resolve(this.db); + } + }); + }); + } + + // 데이터베이스 연결 종료 + close() { + return new Promise((resolve, reject) => { + if (this.db) { + this.db.close((err) => { + if (err) { + reject(err); + } else { + this.db = null; + resolve(); + } + }); + } else { + resolve(); + } + }); + } + + // 모든 파일 목록 가져오기 + async getAllFiles(limit = 100, offset = 0) { + await this.connect(); + + return new Promise((resolve, reject) => { + // 더 간단한 쿼리로 변경 - 첨부파일은 별도 쿼리로 처리 + const query = ` + SELECT * FROM files + ORDER BY created_at DESC + LIMIT ? OFFSET ? + `; + + this.db.all(query, [limit, offset], async (err, rows) => { + if (err) { + reject(err); + return; + } + + const files = []; + + for (const row of rows) { + const file = { + id: row.id, + title: row.title, + description: row.description, + category: row.category, + tags: row.tags ? JSON.parse(row.tags) : [], + user_id: row.user_id, + created_at: row.created_at, + updated_at: row.updated_at, + files: [] + }; + + // 각 파일의 첨부파일을 별도로 조회 + try { + const attachments = await this.getFileAttachments(row.id); + file.files = attachments; + } catch (attachmentError) { + console.warn('첨부파일 조회 오류:', attachmentError); + file.files = []; + } + + files.push(file); + } + + resolve(files); + }); + }); + } + + // 파일의 첨부파일 목록 가져오기 + async getFileAttachments(fileId) { + return new Promise((resolve, reject) => { + const query = 'SELECT * FROM file_attachments WHERE file_id = ?'; + this.db.all(query, [fileId], (err, rows) => { + if (err) { + reject(err); + } else { + const attachments = rows.map(row => ({ + id: row.id, + original_name: row.original_name, + file_name: row.file_name, + file_path: row.file_path, + file_size: row.file_size, + mime_type: row.mime_type, + name: row.original_name, // 호환성을 위해 + size: row.file_size // 호환성을 위해 + })); + resolve(attachments); + } + }); + }); + } + + // 파일 검색 + async searchFiles(searchTerm, category = null, limit = 100) { + await this.connect(); + + return new Promise((resolve, reject) => { + let query = ` + SELECT * FROM files + WHERE (title LIKE ? OR description LIKE ? OR tags LIKE ?) + `; + + const params = [`%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`]; + + if (category) { + query += ' AND category = ?'; + params.push(category); + } + + query += ' ORDER BY created_at DESC LIMIT ?'; + params.push(limit); + + this.db.all(query, params, async (err, rows) => { + if (err) { + reject(err); + return; + } + + const files = []; + + for (const row of rows) { + const file = { + id: row.id, + title: row.title, + description: row.description, + category: row.category, + tags: row.tags ? JSON.parse(row.tags) : [], + user_id: row.user_id, + created_at: row.created_at, + updated_at: row.updated_at, + files: [] + }; + + // 각 파일의 첨부파일을 별도로 조회 + try { + const attachments = await this.getFileAttachments(row.id); + file.files = attachments; + } catch (attachmentError) { + console.warn('첨부파일 조회 오류:', attachmentError); + file.files = []; + } + + files.push(file); + } + + resolve(files); + }); + }); + } + + // 새 파일 추가 + async addFile(fileData) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = ` + INSERT INTO files (id, title, description, category, tags, user_id) + VALUES (?, ?, ?, ?, ?, ?) + `; + + const params = [ + fileData.id || this.generateId(), + fileData.title, + fileData.description || '', + fileData.category, + JSON.stringify(fileData.tags || []), + fileData.user_id || 'offline-user' + ]; + + this.db.run(query, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ id: params[0], changes: this.changes }); + } + }); + }); + } + + // 파일 정보 수정 + async updateFile(id, updates) { + await this.connect(); + + return new Promise((resolve, reject) => { + const setClause = []; + const params = []; + + if (updates.title !== undefined) { + setClause.push('title = ?'); + params.push(updates.title); + } + if (updates.description !== undefined) { + setClause.push('description = ?'); + params.push(updates.description); + } + if (updates.category !== undefined) { + setClause.push('category = ?'); + params.push(updates.category); + } + if (updates.tags !== undefined) { + setClause.push('tags = ?'); + params.push(JSON.stringify(updates.tags)); + } + + setClause.push('updated_at = CURRENT_TIMESTAMP'); + params.push(id); + + const query = `UPDATE files SET ${setClause.join(', ')} WHERE id = ?`; + + this.db.run(query, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + } + + // 파일 삭제 + async deleteFile(id) { + await this.connect(); + + return new Promise((resolve, reject) => { + // 첨부파일부터 삭제 (CASCADE가 있지만 명시적으로) + this.db.run('DELETE FROM file_attachments WHERE file_id = ?', [id], (err) => { + if (err) { + reject(err); + return; + } + + // 파일 정보 삭제 + this.db.run('DELETE FROM files WHERE id = ?', [id], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + }); + } + + // 첨부파일 추가 + async addFileAttachment(fileId, attachmentData) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = ` + INSERT INTO file_attachments (file_id, original_name, file_name, file_path, file_size, mime_type, file_data) + VALUES (?, ?, ?, ?, ?, ?, ?) + `; + + const params = [ + fileId, + attachmentData.original_name, + attachmentData.file_name || attachmentData.original_name, + attachmentData.file_path || '', + attachmentData.file_size || 0, + attachmentData.mime_type || '', + attachmentData.file_data || null + ]; + + this.db.run(query, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ id: this.lastID, changes: this.changes }); + } + }); + }); + } + + // 첨부파일 삭제 + async deleteFileAttachment(attachmentId) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'DELETE FROM file_attachments WHERE id = ?'; + + this.db.run(query, [attachmentId], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + } + + // 카테고리 목록 가져오기 + async getCategories() { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'SELECT * FROM categories ORDER BY is_default DESC, name ASC'; + + this.db.all(query, [], (err, rows) => { + if (err) { + reject(err); + } else { + resolve(rows); + } + }); + }); + } + + // 카테고리 추가 + async addCategory(name) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'INSERT INTO categories (name) VALUES (?)'; + + this.db.run(query, [name], function(err) { + if (err) { + reject(err); + } else { + resolve({ id: this.lastID, changes: this.changes }); + } + }); + }); + } + + // 카테고리 수정 + async updateCategory(id, name) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'UPDATE categories SET name = ? WHERE id = ?'; + + this.db.run(query, [name, id], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + } + + // 카테고리 삭제 + async deleteCategory(id) { + await this.connect(); + + return new Promise((resolve, reject) => { + // 해당 카테고리를 사용하는 파일들을 '기타'로 변경 + this.db.serialize(() => { + this.db.run('UPDATE files SET category = "기타" WHERE category = (SELECT name FROM categories WHERE id = ?)', [id], (err) => { + if (err) { + reject(err); + return; + } + + // 카테고리 삭제 + this.db.run('DELETE FROM categories WHERE id = ?', [id], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + }); + }); + } + + // 통계 정보 가져오기 + async getStats() { + await this.connect(); + + return new Promise((resolve, reject) => { + const queries = [ + 'SELECT COUNT(*) as total_files FROM files', + 'SELECT category, COUNT(*) as count FROM files GROUP BY category', + 'SELECT COUNT(*) as total_attachments FROM file_attachments' + ]; + + Promise.all(queries.map(query => + new Promise((res, rej) => { + this.db.all(query, [], (err, rows) => { + if (err) rej(err); + else res(rows); + }); + }) + )).then(results => { + resolve({ + total_files: results[0][0].total_files, + by_category: results[1], + total_attachments: results[2][0].total_attachments + }); + }).catch(reject); + }); + } + + // 사용자 관련 메서드들 + async getUserByEmail(email) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'SELECT * FROM users WHERE email = ? AND is_active = 1'; + this.db.get(query, [email], (err, row) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); + } + + async getUserById(id) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'SELECT * FROM users WHERE id = ? AND is_active = 1'; + this.db.get(query, [id], (err, row) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); + } + + async createUser(userData) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = ` + INSERT INTO users (id, email, password_hash, name, role) + VALUES (?, ?, ?, ?, ?) + `; + + const userId = this.generateId(); + const params = [ + userId, + userData.email, + userData.password_hash, + userData.name, + userData.role || 'user' + ]; + + this.db.run(query, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ id: userId, changes: this.changes }); + } + }); + }); + } + + async updateUserLastLogin(userId) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?'; + this.db.run(query, [userId], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + } + + async createSession(userId, sessionId, expiresAt) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = ` + INSERT INTO user_sessions (id, user_id, expires_at) + VALUES (?, ?, ?) + `; + + this.db.run(query, [sessionId, userId, expiresAt], function(err) { + if (err) { + reject(err); + } else { + resolve({ id: sessionId, changes: this.changes }); + } + }); + }); + } + + async getSession(sessionId) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = ` + SELECT s.*, u.id as user_id, u.email, u.name, u.role + FROM user_sessions s + JOIN users u ON s.user_id = u.id + WHERE s.id = ? AND s.expires_at > datetime('now') AND u.is_active = 1 + `; + + this.db.get(query, [sessionId], (err, row) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); + } + + async deleteSession(sessionId) { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'DELETE FROM user_sessions WHERE id = ?'; + this.db.run(query, [sessionId], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + } + + async cleanExpiredSessions() { + await this.connect(); + + return new Promise((resolve, reject) => { + const query = 'DELETE FROM user_sessions WHERE expires_at <= datetime("now")'; + this.db.run(query, [], function(err) { + if (err) { + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); + } + + // ID 생성 헬퍼 + generateId() { + return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); + } +} + +module.exports = DatabaseHelper; \ No newline at end of file diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..066d67c --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,90 @@ +-- 자료실 SQLite 데이터베이스 스키마 +-- 파일: database/schema.sql + +-- 파일 정보 테이블 +CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL DEFAULT '기타', + tags TEXT, -- JSON 배열로 저장 + user_id TEXT DEFAULT 'offline-user', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 파일 첨부 정보 테이블 +CREATE TABLE IF NOT EXISTS file_attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id TEXT NOT NULL, + original_name TEXT NOT NULL, + file_name TEXT NOT NULL, -- 실제 저장된 파일명 + file_path TEXT NOT NULL, -- 파일 저장 경로 + file_size INTEGER DEFAULT 0, + mime_type TEXT, + file_data TEXT, -- base64 인코딩된 파일 데이터 (소용량 파일용) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE +); + +-- 카테고리 테이블 +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + is_default INTEGER DEFAULT 0, -- 기본 카테고리 여부 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 사용자 테이블 +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + name TEXT NOT NULL, + role TEXT DEFAULT 'user', -- 'admin', 'user' + is_active INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME +); + +-- 세션 테이블 +CREATE TABLE IF NOT EXISTS user_sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- 기본 카테고리 데이터 삽입 +INSERT OR IGNORE INTO categories (name, is_default) VALUES +('문서', 1), +('이미지', 1), +('동영상', 1), +('프레젠테이션', 1), +('기타', 1); + +-- 기본 관리자 계정 생성 (비밀번호: admin123) +INSERT OR IGNORE INTO users (id, email, password_hash, name, role) VALUES +('admin-001', 'admin@jaryo.com', '$2b$10$0u/zxn1NL4n6t.hNs1eMh.12tXEv9HYgf4cPRXKT3aX97mOKR01Du', '관리자', 'admin'); + +-- 샘플 데이터 삽입 +INSERT OR IGNORE INTO files (id, title, description, category, tags, created_at, updated_at) VALUES +('sample-1', '프로젝트 계획서', '2024년 상반기 주요 프로젝트 계획서입니다.', '문서', '["계획서", "프로젝트", "2024"]', datetime('now'), datetime('now')), +('sample-2', '회의 자료', '월간 정기 회의 자료 모음입니다.', '프레젠테이션', '["회의", "정기회의"]', datetime('now'), datetime('now')), +('sample-3', '시스템 스크린샷', '새로운 관리 시스템 화면 캡처 이미지들입니다.', '이미지', '["시스템", "스크린샷"]', datetime('now'), datetime('now')); + +-- 샘플 첨부파일 데이터 +INSERT OR IGNORE INTO file_attachments (file_id, original_name, file_name, file_path, file_size, mime_type) VALUES +('sample-1', 'project_plan_2024.pdf', 'project_plan_2024.pdf', 'uploads/project_plan_2024.pdf', 1024000, 'application/pdf'), +('sample-2', 'meeting_slides.pptx', 'meeting_slides.pptx', 'uploads/meeting_slides.pptx', 2048000, 'application/vnd.openxmlformats-officedocument.presentationml.presentation'), +('sample-3', 'admin_screenshot1.png', 'admin_screenshot1.png', 'uploads/admin_screenshot1.png', 512000, 'image/png'), +('sample-3', 'admin_screenshot2.png', 'admin_screenshot2.png', 'uploads/admin_screenshot2.png', 768000, 'image/png'); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_files_category ON files(category); +CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at); +CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id); +CREATE INDEX IF NOT EXISTS idx_file_attachments_file_id ON file_attachments(file_id); + diff --git a/debug-files.js b/debug-files.js new file mode 100644 index 0000000..5789cf8 --- /dev/null +++ b/debug-files.js @@ -0,0 +1,68 @@ +const DatabaseHelper = require('./database/db-helper'); +const fs = require('fs'); +const path = require('path'); + +async function debugFiles() { + const db = new DatabaseHelper(); + + try { + await db.connect(); + + console.log('\n📋 데이터베이스의 모든 파일:'); + const files = await db.getAllFiles(); + + files.forEach((file, index) => { + console.log(`\n${index + 1}. ${file.title} (ID: ${file.id})`); + console.log(` 카테고리: ${file.category}`); + console.log(` 첨부파일: ${file.files?.length || 0}개`); + + if (file.files && file.files.length > 0) { + file.files.forEach((attachment, idx) => { + console.log(` ${idx + 1}) ${attachment.original_name}`); + console.log(` - ID: ${attachment.id}`); + console.log(` - 경로: ${attachment.file_path}`); + console.log(` - 파일명: ${attachment.file_name}`); + console.log(` - 크기: ${attachment.file_size}`); + + // 실제 파일 존재 확인 + const fullPath = path.join(__dirname, attachment.file_path); + const exists = fs.existsSync(fullPath); + console.log(` - 실제 파일 존재: ${exists ? '✅' : '❌'} (${fullPath})`); + + if (!exists) { + // 다른 경로들 시도 + const paths = [ + path.join(__dirname, 'uploads', attachment.file_name), + path.join(__dirname, 'uploads', attachment.original_name), + attachment.file_path, + ]; + + console.log(` - 시도할 경로들:`); + paths.forEach(p => { + const pathExists = fs.existsSync(p); + console.log(` ${pathExists ? '✅' : '❌'} ${p}`); + }); + } + }); + } + }); + + console.log('\n📁 uploads 폴더의 실제 파일들:'); + const uploadsDir = path.join(__dirname, 'uploads'); + if (fs.existsSync(uploadsDir)) { + const actualFiles = fs.readdirSync(uploadsDir); + actualFiles.forEach(file => { + const filePath = path.join(uploadsDir, file); + const stats = fs.statSync(filePath); + console.log(` - ${file} (크기: ${stats.size})`); + }); + } + + } catch (error) { + console.error('❌ 오류:', error.message); + } finally { + await db.close(); + } +} + +debugFiles(); \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..95fa5cd --- /dev/null +++ b/deploy.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Git을 통한 자동 배포 스크립트 +# 사용법: ./deploy.sh [branch_name] + +# 설정 +PROJECT_DIR="/volume1/web/jaryo" +GIT_REPO="/volume1/git/jaryo-file-manager.git" +BACKUP_DIR="/volume1/web/jaryo-backup" +LOG_FILE="/volume1/web/jaryo/logs/deploy.log" +BRANCH=${1:-main} + +# 로그 함수 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +# 로그 디렉토리 생성 +mkdir -p "$(dirname $LOG_FILE)" + +log "=== 배포 시작 ===" +log "브랜치: $BRANCH" +log "프로젝트 디렉토리: $PROJECT_DIR" + +# 1. 현재 서비스 중지 +log "기존 서비스 중지 중..." +if [ -f "$PROJECT_DIR/app.pid" ]; then + PID=$(cat "$PROJECT_DIR/app.pid") + if kill -0 "$PID" 2>/dev/null; then + kill "$PID" + sleep 3 + log "서비스 중지 완료 (PID: $PID)" + fi +fi + +# 2. 백업 생성 +log "현재 버전 백업 중..." +BACKUP_NAME="backup-$(date +%Y%m%d-%H%M%S)" +if [ -d "$PROJECT_DIR" ]; then + mkdir -p "$BACKUP_DIR" + cp -r "$PROJECT_DIR" "$BACKUP_DIR/$BACKUP_NAME" + log "백업 완료: $BACKUP_DIR/$BACKUP_NAME" +fi + +# 3. Git에서 최신 코드 가져오기 +log "Git에서 최신 코드 가져오는 중..." +if [ ! -d "$PROJECT_DIR" ]; then + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + git clone "$GIT_REPO" . +else + cd "$PROJECT_DIR" + # 현재 변경사항 백업 + git stash push -m "Auto backup before deploy $(date)" + + # 원격 저장소에서 최신 정보 가져오기 + git fetch origin + + # 지정된 브랜치로 체크아웃 + git checkout "$BRANCH" + + # 원격 브랜치와 동기화 + git pull origin "$BRANCH" +fi + +# 4. 의존성 설치 +log "의존성 설치 중..." +npm install --production + +# 5. 데이터베이스 마이그레이션 (필요한 경우) +log "데이터베이스 초기화 중..." +node scripts/init-database.js + +# 6. 권한 설정 +log "권한 설정 중..." +chmod +x *.sh +chown -R admin:users "$PROJECT_DIR" + +# 7. 서비스 시작 +log "새로운 서비스 시작 중..." +./start-service.sh + +# 8. 서비스 상태 확인 +sleep 5 +if [ -f "$PROJECT_DIR/app.pid" ]; then + PID=$(cat "$PROJECT_DIR/app.pid") + if kill -0 "$PID" 2>/dev/null; then + log "✅ 배포 성공! 서비스가 정상적으로 시작되었습니다. (PID: $PID)" + else + log "❌ 배포 실패! 서비스가 시작되지 않았습니다." + log "로그 확인: tail -f $PROJECT_DIR/logs/app.log" + exit 1 + fi +else + log "❌ 배포 실패! PID 파일이 생성되지 않았습니다." + exit 1 +fi + +# 9. 이전 백업 정리 (30일 이상 된 백업 삭제) +log "오래된 백업 정리 중..." +find "$BACKUP_DIR" -name "backup-*" -type d -mtime +30 -exec rm -rf {} \; 2>/dev/null + +log "=== 배포 완료 ===" +log "서비스 URL: http://$(hostname -I | awk '{print $1}'):3005" diff --git a/index.html b/index.html index 9b01609..975000b 100644 --- a/index.html +++ b/index.html @@ -3,15 +3,17 @@ - 자료실 - CRUD 시스템 + 자료실 - 파일 보기 -
-

📚 자료실 관리 시스템

-

파일과 문서를 효율적으로 관리하세요

+

📚 자료실

+

등록된 자료를 검색하고 다운로드할 수 있습니다

+
@@ -27,7 +29,6 @@
-

📋 자료 목록

@@ -66,10 +67,13 @@
+ +
- - + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fcf1fa0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3105 @@ +{ + "name": "jaryo-file-manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jaryo-file-manager", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-session": "^1.17.3", + "fs": "^0.0.1-security", + "multer": "^1.4.5-lts.1", + "path": "^0.12.7", + "sqlite3": "^5.1.6", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", + "license": "ISC" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..adc9b83 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "jaryo-file-manager", + "version": "1.0.0", + "description": "자료실 파일 관리 시스템", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "init-db": "node scripts/init-database.js" + }, + "dependencies": { + "express": "^4.18.2", + "sqlite3": "^5.1.6", + "cors": "^2.8.5", + "multer": "^1.4.5-lts.1", + "path": "^0.12.7", + "fs": "^0.0.1-security", + "bcrypt": "^5.1.1", + "express-session": "^1.17.3", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "keywords": ["file-manager", "sqlite", "express", "admin"], + "author": "Claude Code", + "license": "MIT" +} \ No newline at end of file diff --git a/pm2-ecosystem.config.js b/pm2-ecosystem.config.js new file mode 100644 index 0000000..2e4d0f0 --- /dev/null +++ b/pm2-ecosystem.config.js @@ -0,0 +1,25 @@ +module.exports = { + apps: [{ + name: 'jaryo-file-manager', + script: 'server.js', + cwd: '/volume1/web/jaryo', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'production', + PORT: 3005 + }, + env_production: { + NODE_ENV: 'production', + PORT: 3005 + }, + log_file: '/volume1/web/jaryo/logs/combined.log', + out_file: '/volume1/web/jaryo/logs/out.log', + error_file: '/volume1/web/jaryo/logs/error.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, + time: true + }] +}; diff --git a/pm2-start.sh b/pm2-start.sh new file mode 100644 index 0000000..f399990 --- /dev/null +++ b/pm2-start.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# PM2를 사용한 시놀로지 NAS 서비스 시작 스크립트 +# 사용법: ./pm2-start.sh + +PROJECT_DIR="/volume1/web/jaryo" +LOG_DIR="/volume1/web/jaryo/logs" + +echo "=== PM2로 Jaryo File Manager 서비스 시작 ===" + +# 로그 디렉토리 생성 +mkdir -p "$LOG_DIR" + +# 프로젝트 디렉토리로 이동 +cd "$PROJECT_DIR" || { + echo "오류: 프로젝트 디렉토리를 찾을 수 없습니다: $PROJECT_DIR" + exit 1 +} + +# PM2 설치 확인 및 설치 +if ! command -v pm2 &> /dev/null; then + echo "PM2 설치 중..." + npm install -g pm2 +fi + +# 의존성 설치 확인 +if [ ! -d "node_modules" ]; then + echo "의존성 설치 중..." + npm install +fi + +# 데이터베이스 초기화 +echo "데이터베이스 초기화 중..." +node scripts/init-database.js + +# 기존 PM2 프로세스 중지 +echo "기존 프로세스 정리 중..." +pm2 delete jaryo-file-manager 2>/dev/null || true + +# PM2로 서비스 시작 +echo "PM2로 서비스 시작 중..." +pm2 start pm2-ecosystem.config.js --env production + +# PM2 시작 스크립트 생성 +pm2 startup +pm2 save + +echo "서비스가 PM2로 시작되었습니다." +echo "상태 확인: pm2 status" +echo "로그 확인: pm2 logs jaryo-file-manager" +echo "서비스 중지: pm2 stop jaryo-file-manager" diff --git a/script.js b/script.js index 9ab1282..246ab21 100644 --- a/script.js +++ b/script.js @@ -1,8 +1,6 @@ -class FileManager { +class PublicFileViewer { constructor() { this.files = []; - this.currentEditId = null; - this.isOnline = navigator.onLine; this.currentPage = 1; this.itemsPerPage = 10; this.filteredFiles = []; @@ -11,16 +9,23 @@ class FileManager { } async init() { - // 오프라인 모드로만 실행 - this.files = this.loadFiles(); - this.filteredFiles = [...this.files]; - this.bindEvents(); - this.renderFiles(); - this.updatePagination(); + try { + this.showLoading(true); + await this.loadFiles(); + this.filteredFiles = [...this.files]; + this.bindEvents(); + this.renderFiles(); + this.updatePagination(); + } catch (error) { + console.error('초기화 오류:', error); + this.showNotification('데이터를 불러오는 중 오류가 발생했습니다.', 'error'); + } finally { + this.showLoading(false); + } } bindEvents() { - // 검색 및 정렬 이벤트만 유지 + // 검색 및 정렬 이벤트 document.getElementById('searchBtn').addEventListener('click', () => this.handleSearch()); document.getElementById('searchInput').addEventListener('keyup', (e) => { if (e.key === 'Enter') this.handleSearch(); @@ -33,8 +38,20 @@ class FileManager { document.getElementById('nextPage').addEventListener('click', () => this.goToNextPage()); } - generateId() { - return Date.now().toString(36) + Math.random().toString(36).substr(2); + async loadFiles() { + try { + const response = await fetch('/api/files/public'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + this.files = data.data || []; + console.log('파일 로드 완료:', this.files.length, '개'); + } catch (error) { + console.error('파일 로드 오류:', error); + this.files = []; + throw error; + } } handleSearch() { @@ -65,7 +82,7 @@ class FileManager { const fileList = document.getElementById('fileList'); const sortBy = document.getElementById('sortBy').value; - // 정렬 (관리자 페이지와 동일하게) + // 정렬 const sortedFiles = [...this.filteredFiles].sort((a, b) => { switch (sortBy) { case 'title': @@ -74,7 +91,7 @@ class FileManager { return a.category.localeCompare(b.category); case 'date': default: - return new Date(b.created_at || b.createdAt) - new Date(a.created_at || a.createdAt); + return new Date(b.created_at) - new Date(a.created_at); } }); @@ -98,7 +115,7 @@ class FileManager { } createFileRowHTML(file, rowNumber) { - const createdDate = new Date(file.created_at || file.createdAt).toLocaleDateString('ko-KR'); + const createdDate = new Date(file.created_at).toLocaleDateString('ko-KR'); const hasAttachments = file.files && file.files.length > 0; return ` @@ -108,32 +125,31 @@ class FileManager { ${file.category} -
+
${this.escapeHtml(file.title)} ${file.description ? `
${this.escapeHtml(file.description)}` : ''} ${file.tags && file.tags.length > 0 ? - `
${file.tags.map(tag => `#${this.escapeHtml(tag)}`).join('')}
` : '' + `
${this.parseJsonTags(file.tags).map(tag => `#${this.escapeHtml(tag)}`).join('')}
` : '' }
${hasAttachments ? - `
${file.files.map((f, index) => - `
- ${this.getFileIcon(f.name || f.original_name || 'unknown')} -
-
${this.escapeHtml(f.name || f.original_name || '파일')}
-
${this.formatFileSize(f.size || 0)}
-
-
` - ).join('')}
` : + `
+ ${file.files.map((f, index) => + `
+ ${this.getFileIcon(f.original_name || 'unknown')} + ${this.escapeHtml(f.original_name || '파일')} +
` + ).join('')} +
` : `-` } ${createdDate} ${hasAttachments ? - `` : + `` : `-` } @@ -141,6 +157,17 @@ class FileManager { `; } + parseJsonTags(tags) { + try { + if (typeof tags === 'string') { + return JSON.parse(tags); + } + return Array.isArray(tags) ? tags : []; + } catch (error) { + return []; + } + } + getFileIcon(fileName) { const ext = fileName.split('.').pop().toLowerCase(); const iconMap = { @@ -173,17 +200,16 @@ class FileManager { } showDetailView(file) { - // 메인 컨테이너 숨기기 const container = document.querySelector('.container'); container.style.display = 'none'; - // 상세보기 컨테이너 생성 const detailContainer = document.createElement('div'); detailContainer.className = 'detail-container'; detailContainer.id = 'detailContainer'; - const createdDate = new Date(file.created_at || file.createdAt).toLocaleDateString('ko-KR'); - const updatedDate = new Date(file.updated_at || file.updatedAt).toLocaleDateString('ko-KR'); + const createdDate = new Date(file.created_at).toLocaleDateString('ko-KR'); + const updatedDate = new Date(file.updated_at).toLocaleDateString('ko-KR'); + const tags = this.parseJsonTags(file.tags); detailContainer.innerHTML = `
@@ -195,7 +221,7 @@ class FileManager {

📄 ${this.escapeHtml(file.title)}

-
@@ -216,12 +242,12 @@ class FileManager {
- ${file.tags && file.tags.length > 0 ? ` + ${tags && tags.length > 0 ? `
- ${file.tags.map(tag => `#${this.escapeHtml(tag)}`).join('')} + ${tags.map(tag => `#${this.escapeHtml(tag)}`).join('')}
` : ''} @@ -233,16 +259,16 @@ class FileManager {
${file.files.map((f, index) => `
- ${this.getFileIcon(f.name || f.original_name || 'unknown')} - ${this.escapeHtml(f.name || f.original_name || '파일')} -
`).join('')}
-
@@ -280,18 +306,13 @@ class FileManager { detailContainer.remove(); } - // 메인 컨테이너 다시 보이기 const container = document.querySelector('.container'); container.style.display = 'block'; } async downloadFiles(id) { const file = this.files.find(f => f.id === id); - if (!file) { - this.showNotification('파일을 찾을 수 없습니다.', 'error'); - return; - } - if (!file.files || file.files.length === 0) { + if (!file || !file.files || file.files.length === 0) { this.showNotification('첨부파일이 없습니다.', 'error'); return; } @@ -299,12 +320,13 @@ class FileManager { try { if (file.files.length === 1) { // 단일 파일: 직접 다운로드 - await this.downloadSingleFileData(file.files[0]); - this.showNotification(`파일 다운로드 완료: ${file.files[0].name || file.files[0].original_name}`, 'success'); + await this.downloadSingleFile(id, 0); } else { - // 다중 파일: localStorage에서 base64 데이터를 각각 다운로드 - for (const fileData of file.files) { - await this.downloadSingleFileData(fileData); + // 다중 파일: 각각 다운로드 + for (let i = 0; i < file.files.length; i++) { + await this.downloadSingleFile(id, i); + // 짧은 딜레이를 추가하여 브라우저가 다운로드를 처리할 시간을 줌 + await new Promise(resolve => setTimeout(resolve, 500)); } this.showNotification(`${file.files.length}개 파일 다운로드 완료`, 'success'); } @@ -314,41 +336,134 @@ class FileManager { } } - async downloadSingleFile(fileId, fileIndex) { - const file = this.files.find(f => f.id === fileId); - if (!file) { - this.showNotification('파일을 찾을 수 없습니다.', 'error'); - return; - } - if (!file.files || !file.files[fileIndex]) { - this.showNotification('첨부파일을 찾을 수 없습니다.', 'error'); - return; - } - + async downloadSingleFile(fileId, attachmentIndex) { try { - const fileData = file.files[fileIndex]; - await this.downloadSingleFileData(fileData); - this.showNotification(`파일 다운로드 완료: ${fileData.name || fileData.original_name}`, 'success'); + // 다운로드 시작 로딩 표시 + this.showLoading(true); + + console.log('downloadSingleFile 호출됨:', fileId, attachmentIndex); + const file = this.files.find(f => f.id === fileId); + console.log('찾은 파일:', file); + + if (!file || !file.files[attachmentIndex]) { + console.log('파일 또는 첨부파일을 찾을 수 없음'); + throw new Error('파일을 찾을 수 없습니다.'); + } + + const attachmentId = file.files[attachmentIndex].id; + const downloadUrl = `/api/download/${fileId}/${attachmentId}`; + console.log('다운로드 URL:', downloadUrl); + + const response = await fetch(downloadUrl, { + credentials: 'include' + }); + console.log('응답 상태:', response.status, response.statusText); + + if (!response.ok) { + const errorText = await response.text(); + console.log('응답 오류:', errorText); + throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); + } + + console.log('다운로드 시작...'); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + // 파일명을 서버에서 전송된 정보에서 추출 (개선된 방식) + const contentDisposition = response.headers.get('Content-Disposition'); + let filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`; + + console.log('📁 다운로드 파일명 처리:', { + original_name: file.files[attachmentIndex].original_name, + content_disposition: contentDisposition, + default_filename: filename + }); + + if (contentDisposition) { + // RFC 5987 filename* 파라미터를 우선 처리 (UTF-8 지원) + const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/); + if (filenameStarMatch) { + filename = decodeURIComponent(filenameStarMatch[1]); + console.log('📁 UTF-8 파일명 추출:', filename); + } else { + // 일반 filename 파라미터 처리 + const filenameMatch = contentDisposition.match(/filename="?([^";\r\n]+)"?/); + if (filenameMatch) { + filename = filenameMatch[1]; + console.log('📁 기본 파일명 추출:', filename); + } + } + } + + // 파일명이 여전히 비어있다면 기본값 사용 + if (!filename || filename.trim() === '') { + filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`; + console.log('📁 기본 파일명 사용:', filename); + } + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + console.log('다운로드 완료'); + this.showLoading(false); + + if (arguments.length === 2) { // 단일 파일 다운로드인 경우만 알림 표시 + this.showNotification(`파일 다운로드 완료: ${filename}`, 'success'); + } } catch (error) { - console.error('개별 파일 다운로드 오류:', error); + console.error('downloadSingleFile 오류:', error); + this.showLoading(false); this.showNotification('파일 다운로드 중 오류가 발생했습니다.', 'error'); } } - async downloadSingleFileData(fileData) { - if (fileData.data) { - // localStorage의 base64 데이터 다운로드 - const link = document.createElement('a'); - link.href = fileData.data; - link.download = fileData.name || fileData.original_name || 'file'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + updatePagination() { + const totalPages = Math.max(1, Math.ceil(this.filteredFiles.length / this.itemsPerPage)); + const pagination = document.getElementById('pagination'); + const prevBtn = document.getElementById('prevPage'); + const nextBtn = document.getElementById('nextPage'); + const pageInfo = document.getElementById('pageInfo'); + + pagination.style.display = 'flex'; + + prevBtn.disabled = this.currentPage <= 1; + nextBtn.disabled = this.currentPage >= totalPages || this.filteredFiles.length === 0; + + const displayTotalPages = this.filteredFiles.length === 0 ? 1 : totalPages; + const displayCurrentPage = this.filteredFiles.length === 0 ? 1 : this.currentPage; + pageInfo.textContent = `${displayCurrentPage} / ${displayTotalPages}`; + } + + goToPrevPage() { + if (this.currentPage > 1) { + this.currentPage--; + this.renderFiles(); + this.updatePagination(); + } + } + + goToNextPage() { + const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage); + if (this.currentPage < totalPages) { + this.currentPage++; + this.renderFiles(); + this.updatePagination(); + } + } + + showLoading(show) { + const loadingEl = document.getElementById('loadingMessage'); + if (loadingEl) { + loadingEl.style.display = show ? 'block' : 'none'; } } showNotification(message, type = 'info') { - // 간단한 알림 표시 const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; @@ -374,102 +489,15 @@ class FileManager { }, 3000); } - updatePagination() { - const totalPages = Math.max(1, Math.ceil(this.filteredFiles.length / this.itemsPerPage)); - const pagination = document.getElementById('pagination'); - const prevBtn = document.getElementById('prevPage'); - const nextBtn = document.getElementById('nextPage'); - const pageInfo = document.getElementById('pageInfo'); - - // 항상 페이지네이션을 표시 - pagination.style.display = 'flex'; - - // 페이지 버튼 상태 업데이트 - prevBtn.disabled = this.currentPage <= 1; - nextBtn.disabled = this.currentPage >= totalPages || this.filteredFiles.length === 0; - - // 페이지 정보 표시 (아이템이 없어도 1/1로 표시) - const displayTotalPages = this.filteredFiles.length === 0 ? 1 : totalPages; - const displayCurrentPage = this.filteredFiles.length === 0 ? 1 : this.currentPage; - pageInfo.textContent = `${displayCurrentPage} / ${displayTotalPages}`; - } - - goToPrevPage() { - if (this.currentPage > 1) { - this.currentPage--; - this.renderFiles(); - this.updatePagination(); - } - } - - goToNextPage() { - const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage); - if (this.currentPage < totalPages) { - this.currentPage++; - this.renderFiles(); - this.updatePagination(); - } - } - - - loadFiles() { - try { - const stored = localStorage.getItem('fileManagerData'); - const files = stored ? JSON.parse(stored) : []; - - // 기존 localStorage 데이터의 호환성을 위해 컴럼명 변환 - return files.map(file => ({ - ...file, - created_at: file.created_at || file.createdAt || new Date().toISOString(), - updated_at: file.updated_at || file.updatedAt || new Date().toISOString() - })); - } catch (error) { - console.error('파일 로드 중 오류:', error); - return []; - } - } - - saveFiles() { - try { - localStorage.setItem('fileManagerData', JSON.stringify(this.files)); - } catch (error) { - console.error('파일 저장 중 오류:', error); - } - } - escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } - - showMessage(message, type = 'success') { - const messageEl = document.createElement('div'); - messageEl.className = `message ${type}`; - messageEl.textContent = message; - messageEl.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: ${type === 'error' ? '#ef4444' : '#10b981'}; - color: white; - padding: 1rem 1.5rem; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); - z-index: 1000; - animation: slideIn 0.3s ease-out; - `; - - document.body.appendChild(messageEl); - setTimeout(() => { - messageEl.style.animation = 'slideOut 0.3s ease-out'; - setTimeout(() => messageEl.remove(), 300); - }, 3000); - } } // 전역 인스턴스 생성 -let fileManager; +let publicViewer; document.addEventListener('DOMContentLoaded', () => { - fileManager = new FileManager(); + publicViewer = new PublicFileViewer(); }); \ No newline at end of file diff --git a/scripts/init-database.js b/scripts/init-database.js new file mode 100644 index 0000000..7ec2682 --- /dev/null +++ b/scripts/init-database.js @@ -0,0 +1,73 @@ +const fs = require('fs'); +const path = require('path'); +const sqlite3 = require('sqlite3').verbose(); + +// 데이터베이스 파일 경로 +const dbPath = path.join(__dirname, '../database/jaryo.db'); +const schemaPath = path.join(__dirname, '../database/schema.sql'); + +// database 폴더가 없으면 생성 +const dbDir = path.dirname(dbPath); +if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); +} + +// uploads 폴더도 생성 +const uploadsDir = path.join(__dirname, '../uploads'); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +console.log('🔧 SQLite 데이터베이스 초기화 시작...'); + +// 데이터베이스 연결 +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error('❌ 데이터베이스 연결 오류:', err.message); + return; + } + console.log('✅ SQLite 데이터베이스 연결 성공'); +}); + +// 스키마 파일 읽기 및 실행 +fs.readFile(schemaPath, 'utf8', (err, schema) => { + if (err) { + console.error('❌ 스키마 파일 읽기 오류:', err.message); + return; + } + + // 여러 SQL 문을 분리하여 실행 + const statements = schema.split(';').filter(stmt => stmt.trim().length > 0); + + db.serialize(() => { + statements.forEach((statement, index) => { + if (statement.trim()) { + db.run(statement + ';', (err) => { + if (err) { + console.error(`❌ SQL 실행 오류 (${index + 1}):`, err.message); + console.error('실행하려던 SQL:', statement); + } + }); + } + }); + + console.log('✅ 데이터베이스 스키마 생성 완료'); + + // 데이터 확인 + db.all('SELECT COUNT(*) as count FROM files', (err, rows) => { + if (err) { + console.error('❌ 데이터 확인 오류:', err.message); + } else { + console.log(`📊 파일 테이블 레코드 수: ${rows[0].count}`); + } + + db.close((err) => { + if (err) { + console.error('❌ 데이터베이스 종료 오류:', err.message); + } else { + console.log('🏁 데이터베이스 초기화 완료'); + } + }); + }); + }); +}); \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..9b854ae --- /dev/null +++ b/server.js @@ -0,0 +1,788 @@ +const express = require('express'); +const cors = require('cors'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const bcrypt = require('bcrypt'); +const session = require('express-session'); +const { v4: uuidv4 } = require('uuid'); +const DatabaseHelper = require('./database/db-helper'); + +const app = express(); +const PORT = process.env.PORT || 3005; + +// 데이터베이스 헬퍼 인스턴스 +const db = new DatabaseHelper(); + +// 미들웨어 설정 +app.use(cors({ + origin: ['http://localhost:3001', 'http://127.0.0.1:3001'], + credentials: true +})); +app.use(express.json({ limit: '50mb' })); +app.use(express.urlencoded({ extended: true, limit: '50mb' })); + +// 세션 설정 +app.use(session({ + secret: 'jaryo-file-manager-secret-key-2024', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // HTTPS에서는 true로 설정 + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24시간 + } +})); + +// 모든 요청 로깅 미들웨어 +app.use((req, res, next) => { + if (req.url.includes('/api/categories')) { + console.log(`📨 ${req.method} ${req.url} - Time: ${new Date().toISOString()}`); + console.log('Headers:', req.headers); + console.log('Body:', req.body); + } + next(); +}); + +// 정적 파일 서빙 +app.use(express.static(__dirname)); +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// 루트 경로에서 메인 페이지로 리다이렉트 +app.get('/', (req, res) => { + res.redirect('/index.html'); +}); + +// Multer 설정 (파일 업로드) +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadDir = 'uploads'; + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + // 한글 파일명 처리를 위해 Buffer로 디코딩 + const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const extension = path.extname(originalName); + const baseName = path.basename(originalName, extension); + + // 안전한 파일명 생성 (특수문자 제거) + const safeName = baseName.replace(/[<>:"/\\|?*]/g, '_'); + cb(null, safeName + '-' + uniqueSuffix + extension); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 2 * 1024 * 1024 * 1024, // 2GB 제한 + files: 20, // 최대 파일 개수 + fieldSize: 100 * 1024 * 1024 // 필드 크기 제한 + }, + fileFilter: (req, file, cb) => { + // 모든 파일 타입 허용 + cb(null, true); + } +}); + +// 인증 미들웨어 +const requireAuth = async (req, res, next) => { + try { + if (!req.session.userId) { + return res.status(401).json({ + success: false, + error: '로그인이 필요합니다.' + }); + } + + const user = await db.getUserById(req.session.userId); + if (!user) { + req.session.destroy(); + return res.status(401).json({ + success: false, + error: '유효하지 않은 세션입니다.' + }); + } + + req.user = user; + next(); + } catch (error) { + console.error('인증 미들웨어 오류:', error); + res.status(500).json({ + success: false, + error: '인증 처리 중 오류가 발생했습니다.' + }); + } +}; + +// 관리자 권한 확인 미들웨어 +const requireAdmin = (req, res, next) => { + if (!req.user || req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + error: '관리자 권한이 필요합니다.' + }); + } + next(); +}; + +// API 라우트 + +// 회원가입 +app.post('/api/auth/signup', async (req, res) => { + try { + const { email, password, name } = req.body; + + if (!email || !password || !name) { + return res.status(400).json({ + success: false, + error: '이메일, 비밀번호, 이름은 필수입니다.' + }); + } + + // 이메일 중복 확인 + const existingUser = await db.getUserByEmail(email); + if (existingUser) { + return res.status(400).json({ + success: false, + error: '이미 등록된 이메일입니다.' + }); + } + + // 비밀번호 해시화 + const saltRounds = 10; + const password_hash = await bcrypt.hash(password, saltRounds); + + // 사용자 생성 + const result = await db.createUser({ + email, + password_hash, + name, + role: 'user' + }); + + res.json({ + success: true, + message: '회원가입이 완료되었습니다.', + data: { id: result.id } + }); + } catch (error) { + console.error('회원가입 오류:', error); + res.status(500).json({ + success: false, + error: '회원가입 중 오류가 발생했습니다.' + }); + } +}); + +// 로그인 +app.post('/api/auth/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + success: false, + error: '이메일과 비밀번호를 입력해주세요.' + }); + } + + // 사용자 확인 + const user = await db.getUserByEmail(email); + if (!user) { + return res.status(401).json({ + success: false, + error: '이메일 또는 비밀번호가 올바르지 않습니다.' + }); + } + + // 비밀번호 확인 + const isValidPassword = await bcrypt.compare(password, user.password_hash); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + error: '이메일 또는 비밀번호가 올바르지 않습니다.' + }); + } + + // 세션 설정 + req.session.userId = user.id; + req.session.userEmail = user.email; + req.session.userName = user.name; + req.session.userRole = user.role; + + // 마지막 로그인 시간 업데이트 + await db.updateUserLastLogin(user.id); + + res.json({ + success: true, + message: '로그인 성공', + data: { + id: user.id, + email: user.email, + name: user.name, + role: user.role + } + }); + } catch (error) { + console.error('로그인 오류:', error); + res.status(500).json({ + success: false, + error: '로그인 중 오류가 발생했습니다.' + }); + } +}); + +// 로그아웃 +app.post('/api/auth/logout', (req, res) => { + req.session.destroy((err) => { + if (err) { + console.error('로그아웃 오류:', err); + return res.status(500).json({ + success: false, + error: '로그아웃 중 오류가 발생했습니다.' + }); + } + + res.json({ + success: true, + message: '로그아웃되었습니다.' + }); + }); +}); + +// 현재 사용자 정보 조회 +app.get('/api/auth/me', requireAuth, (req, res) => { + res.json({ + success: true, + data: { + id: req.user.id, + email: req.user.email, + name: req.user.name, + role: req.user.role, + last_login: req.user.last_login + } + }); +}); + +// 세션 상태 확인 +app.get('/api/auth/session', async (req, res) => { + try { + if (!req.session.userId) { + return res.json({ + success: true, + user: null + }); + } + + const user = await db.getUserById(req.session.userId); + if (!user) { + req.session.destroy(); + return res.json({ + success: true, + user: null + }); + } + + res.json({ + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + last_login: user.last_login + } + }); + } catch (error) { + console.error('세션 확인 오류:', error); + res.status(500).json({ + success: false, + error: '세션 확인 중 오류가 발생했습니다.' + }); + } +}); + +// 파일 목록 조회 (관리자용) +app.get('/api/files', async (req, res) => { + try { + const { search, category, limit = 100, offset = 0 } = req.query; + + let files; + if (search) { + files = await db.searchFiles(search, category, parseInt(limit)); + } else { + files = await db.getAllFiles(parseInt(limit), parseInt(offset)); + } + + res.json({ + success: true, + data: files, + count: files.length + }); + } catch (error) { + console.error('파일 목록 조회 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 공개 파일 목록 조회 (일반 사용자용) +app.get('/api/files/public', async (req, res) => { + try { + const { search, category, limit = 100, offset = 0 } = req.query; + + let files; + if (search) { + files = await db.searchFiles(search, category, parseInt(limit)); + } else { + files = await db.getAllFiles(parseInt(limit), parseInt(offset)); + } + + res.json({ + success: true, + data: files, + count: files.length + }); + } catch (error) { + console.error('공개 파일 목록 조회 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 파일 추가 +app.post('/api/files', requireAuth, upload.array('files'), async (req, res) => { + try { + const { title, description, category, tags } = req.body; + + if (!title || !category) { + return res.status(400).json({ + success: false, + error: '제목과 카테고리는 필수입니다.' + }); + } + + const fileId = db.generateId(); + const fileData = { + id: fileId, + title, + description, + category, + tags: tags ? (typeof tags === 'string' ? JSON.parse(tags) : tags) : [], + user_id: req.user.id + }; + + // 파일 정보 저장 + const result = await db.addFile(fileData); + + // 첨부파일 처리 + if (req.files && req.files.length > 0) { + for (const file of req.files) { + // 한글 파일명 처리 + const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + + await db.addFileAttachment(fileId, { + original_name: originalName, + file_name: file.filename, + file_path: file.path, + file_size: file.size, + mime_type: file.mimetype + }); + } + } + + res.json({ + success: true, + data: { id: fileId, ...result } + }); + } catch (error) { + console.error('파일 추가 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 파일 수정 +app.put('/api/files/:id', requireAuth, upload.array('files'), async (req, res) => { + try { + const { id } = req.params; + const { title, description, category, tags, filesToDelete } = req.body; + + console.log('🔄 파일 업데이트 시작:', id); + console.log('📋 업데이트 데이터:', { title, description, category, tags }); + console.log('🗑️ 삭제할 첨부파일:', filesToDelete); + console.log('📎 새 첨부파일 개수:', req.files ? req.files.length : 0); + + // 기본 파일 정보 업데이트 + const updates = { + title, + description, + category, + tags: tags ? (typeof tags === 'string' ? tags : JSON.stringify(tags)) : '[]' + }; + + const result = await db.updateFile(id, updates); + + if (result.changes === 0) { + return res.status(404).json({ + success: false, + error: '파일을 찾을 수 없습니다.' + }); + } + + // 첨부파일 삭제 처리 + if (filesToDelete) { + const deleteIds = typeof filesToDelete === 'string' ? JSON.parse(filesToDelete) : filesToDelete; + console.log('삭제 처리할 첨부파일 ID들:', deleteIds); + + if (Array.isArray(deleteIds) && deleteIds.length > 0) { + for (const attachmentId of deleteIds) { + try { + // 첨부파일 정보 조회 + const attachments = await db.getFileAttachments(id); + const attachment = attachments.find(a => a.id == attachmentId); + + if (attachment) { + // 실제 파일 삭제 + const filePath = path.join(__dirname, attachment.file_path); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + console.log('실제 파일 삭제됨:', filePath); + } + + // 데이터베이스에서 첨부파일 삭제 + await db.deleteFileAttachment(attachmentId); + console.log('DB에서 첨부파일 삭제됨:', attachmentId); + } + } catch (deleteError) { + console.error('첨부파일 삭제 오류:', deleteError); + } + } + } + } + + // 새 첨부파일 추가 + if (req.files && req.files.length > 0) { + console.log('새 첨부파일 추가 시작'); + for (const file of req.files) { + try { + // 한글 파일명 처리 + const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + + await db.addFileAttachment(id, { + original_name: originalName, + file_name: file.filename, + file_path: file.path, + file_size: file.size, + mime_type: file.mimetype + }); + + console.log('새 첨부파일 추가됨:', originalName); + } catch (addError) { + console.error('새 첨부파일 추가 오류:', addError); + } + } + } + + console.log('✅ 파일 업데이트 완료'); + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('파일 수정 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 파일 삭제 +app.delete('/api/files/:id', requireAuth, async (req, res) => { + try { + const { id } = req.params; + + const result = await db.deleteFile(id); + + if (result.changes === 0) { + return res.status(404).json({ + success: false, + error: '파일을 찾을 수 없습니다.' + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('파일 삭제 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 카테고리 목록 조회 +app.get('/api/categories', async (req, res) => { + try { + const categories = await db.getCategories(); + res.json({ + success: true, + data: categories + }); + } catch (error) { + console.error('카테고리 조회 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 테스트용 엔드포인트 +app.get('/api/categories/test', (req, res) => { + console.log('📋 테스트 엔드포인트 호출됨'); + res.json({ + success: true, + message: '테스트 성공', + timestamp: new Date().toISOString() + }); +}); + +// 카테고리 추가 +app.post('/api/categories', async (req, res) => { + try { + const { name } = req.body; + + if (!name) { + return res.status(400).json({ + success: false, + error: '카테고리 이름은 필수입니다.' + }); + } + + const result = await db.addCategory(name); + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('카테고리 추가 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 카테고리 수정 +app.put('/api/categories/:id', async (req, res) => { + try { + console.log('🔄 카테고리 수정 요청 받음'); + console.log('URL:', req.url); + console.log('Method:', req.method); + console.log('Params:', req.params); + console.log('Body:', req.body); + + const { id } = req.params; + const { name } = req.body; + + console.log('추출된 ID:', id, 'Type:', typeof id); + console.log('추출된 name:', name); + + if (!name) { + console.log('❌ 카테고리 이름이 없음'); + return res.status(400).json({ + success: false, + error: '카테고리 이름은 필수입니다.' + }); + } + + console.log('📝 데이터베이스 업데이트 시작'); + const result = await db.updateCategory(id, name); + console.log('📝 데이터베이스 업데이트 결과:', result); + + if (result.changes === 0) { + console.log('❌ 변경된 행이 없음 - 카테고리를 찾을 수 없음'); + return res.status(404).json({ + success: false, + error: '카테고리를 찾을 수 없습니다.' + }); + } + + console.log('✅ 카테고리 수정 성공'); + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('카테고리 수정 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 카테고리 삭제 +app.delete('/api/categories/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.deleteCategory(id); + + if (result.changes === 0) { + return res.status(404).json({ + success: false, + error: '카테고리를 찾을 수 없습니다.' + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('카테고리 삭제 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 통계 정보 조회 +app.get('/api/stats', async (req, res) => { + try { + const stats = await db.getStats(); + res.json({ + success: true, + data: stats + }); + } catch (error) { + console.error('통계 조회 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 파일 다운로드 +app.get('/api/download/:id/:attachmentId', async (req, res) => { + try { + const { id, attachmentId } = req.params; + + // 첨부파일 정보 조회 (간단한 쿼리로 대체) + await db.connect(); + const query = 'SELECT * FROM file_attachments WHERE id = ? AND file_id = ?'; + + db.db.get(query, [attachmentId, id], (err, row) => { + if (err) { + console.error('파일 조회 오류:', err); + return res.status(500).json({ + success: false, + error: err.message + }); + } + + if (!row) { + return res.status(404).json({ + success: false, + error: '파일을 찾을 수 없습니다.' + }); + } + + const filePath = path.join(__dirname, row.file_path); + + if (fs.existsSync(filePath)) { + // 한글 파일명을 위한 개선된 헤더 설정 + console.log('📁 다운로드 파일 정보:', { + original_name: row.original_name, + file_path: row.file_path, + storage_path: filePath + }); + + const originalName = row.original_name || 'download'; + const encodedName = encodeURIComponent(originalName); + + // RFC 5987을 준수하는 헤더 설정 (한글 파일명 지원) + res.setHeader('Content-Disposition', + `attachment; filename*=UTF-8''${encodedName}`); + res.setHeader('Content-Type', row.mime_type || 'application/octet-stream'); + res.setHeader('Content-Length', row.file_size || fs.statSync(filePath).size); + + // 원본 파일명으로 다운로드 + res.download(filePath, originalName, (err) => { + if (err) { + console.error('📁 다운로드 오류:', err); + } else { + console.log('📁 다운로드 완료:', originalName); + } + }); + } else { + res.status(404).json({ + success: false, + error: '파일이 존재하지 않습니다.' + }); + } + }); + } catch (error) { + console.error('파일 다운로드 오류:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 에러 핸들러 +app.use((error, req, res, next) => { + console.error('서버 오류:', error); + res.status(500).json({ + success: false, + error: '서버 내부 오류가 발생했습니다.' + }); +}); + +// 404 핸들러 +app.use((req, res) => { + res.status(404).json({ + success: false, + error: '요청한 리소스를 찾을 수 없습니다.' + }); +}); + +// 서버 시작 +app.listen(PORT, () => { + console.log(`🚀 자료실 서버가 포트 ${PORT}에서 실행중입니다.`); + console.log(`📱 Admin 페이지: http://localhost:${PORT}/admin/index.html`); + console.log(`🌐 Main 페이지: http://localhost:${PORT}/index.html`); + console.log(`📊 API: http://localhost:${PORT}/api/files`); +}); + +// 프로세스 종료 시 데이터베이스 연결 종료 +process.on('SIGINT', async () => { + console.log('\n📝 서버를 종료합니다...'); + await db.close(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('\n📝 서버를 종료합니다...'); + await db.close(); + process.exit(0); +}); \ No newline at end of file diff --git a/start-service.sh b/start-service.sh new file mode 100644 index 0000000..2085267 --- /dev/null +++ b/start-service.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# 시놀로지 NAS용 서비스 시작 스크립트 +# 사용법: ./start-service.sh + +# 프로젝트 디렉토리 설정 (실제 경로에 맞게 수정) +PROJECT_DIR="/volume1/web/jaryo" +LOG_FILE="/volume1/web/jaryo/logs/app.log" +PID_FILE="/volume1/web/jaryo/app.pid" + +# 로그 디렉토리 생성 +mkdir -p "$(dirname $LOG_FILE)" + +# Node.js 경로 확인 (시놀로지 기본 설치 경로) +NODE_PATH="/volume1/@appstore/Node.js_v18/usr/local/bin/node" +if [ ! -f "$NODE_PATH" ]; then + NODE_PATH="node" +fi + +# NPM 경로 확인 +NPM_PATH="/volume1/@appstore/Node.js_v18/usr/local/bin/npm" +if [ ! -f "$NPM_PATH" ]; then + NPM_PATH="npm" +fi + +echo "=== Jaryo File Manager 서비스 시작 ===" +echo "프로젝트 디렉토리: $PROJECT_DIR" +echo "Node.js 경로: $NODE_PATH" +echo "로그 파일: $LOG_FILE" + +# 프로젝트 디렉토리로 이동 +cd "$PROJECT_DIR" || { + echo "오류: 프로젝트 디렉토리를 찾을 수 없습니다: $PROJECT_DIR" + exit 1 +} + +# 의존성 설치 확인 +if [ ! -d "node_modules" ]; then + echo "의존성 설치 중..." + $NPM_PATH install +fi + +# 데이터베이스 초기화 +echo "데이터베이스 초기화 중..." +$NODE_PATH scripts/init-database.js + +# 기존 프로세스 종료 +if [ -f "$PID_FILE" ]; then + OLD_PID=$(cat "$PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "기존 프로세스 종료 중 (PID: $OLD_PID)..." + kill "$OLD_PID" + sleep 2 + fi + rm -f "$PID_FILE" +fi + +# 서비스 시작 +echo "서비스 시작 중..." +nohup $NODE_PATH server.js > "$LOG_FILE" 2>&1 & +NEW_PID=$! + +# PID 저장 +echo $NEW_PID > "$PID_FILE" + +echo "서비스가 시작되었습니다. PID: $NEW_PID" +echo "로그 확인: tail -f $LOG_FILE" +echo "서비스 중지: kill $NEW_PID" diff --git a/stop-service.sh b/stop-service.sh new file mode 100644 index 0000000..fdabac5 --- /dev/null +++ b/stop-service.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# 시놀로지 NAS용 서비스 중지 스크립트 +# 사용법: ./stop-service.sh + +PROJECT_DIR="/volume1/web/jaryo" +PID_FILE="/volume1/web/jaryo/app.pid" + +echo "=== Jaryo File Manager 서비스 중지 ===" + +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + echo "프로세스 ID: $PID" + + if kill -0 "$PID" 2>/dev/null; then + echo "서비스 중지 중..." + kill "$PID" + sleep 2 + + # 강제 종료 확인 + if kill -0 "$PID" 2>/dev/null; then + echo "강제 종료 중..." + kill -9 "$PID" + fi + + echo "서비스가 중지되었습니다." + else + echo "프로세스가 이미 종료되었습니다." + fi + + rm -f "$PID_FILE" +else + echo "PID 파일을 찾을 수 없습니다. 수동으로 프로세스를 확인하세요." + echo "실행 중인 Node.js 프로세스:" + ps aux | grep "node server.js" | grep -v grep +fi diff --git a/styles.css b/styles.css index c10cf1e..9898fde 100644 --- a/styles.css +++ b/styles.css @@ -39,6 +39,27 @@ header p { font-size: 1.1rem; } +.admin-link { + margin-top: 15px; +} + +.admin-btn { + display: inline-block; + padding: 8px 16px; + background: #667eea; + color: white; + text-decoration: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.admin-btn:hover { + background: #5a67d8; + transform: translateY(-1px); +} + /* 페이지네이션 스타일 */ .auth-section { @@ -46,6 +67,83 @@ header p { display: flex; justify-content: center; align-items: center; + flex-direction: column; + gap: 15px; +} + +.auth-form { + background: rgba(255, 255, 255, 0.9); + padding: 25px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; +} + +.auth-form h3 { + margin-bottom: 20px; + color: #4a5568; + font-size: 1.4rem; + text-align: center; +} + +.auth-form input { + width: 100%; + padding: 12px 15px; + margin-bottom: 15px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1rem; + transition: all 0.3s ease; +} + +.auth-form input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.auth-toggle { + text-align: center; + margin-top: 15px; + color: #666; + font-size: 0.9rem; +} + +.auth-toggle a { + color: #667eea; + text-decoration: none; + font-weight: 500; +} + +.auth-toggle a:hover { + text-decoration: underline; +} + +.connection-status { + display: flex; + align-items: center; + gap: 8px; + background: rgba(255, 255, 255, 0.8); + padding: 8px 12px; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + font-size: 1.2rem; +} + +.status-indicator.online { + color: #48bb78; +} + +.status-indicator.offline { + color: #ed8936; } .auth-buttons { @@ -243,6 +341,97 @@ header p { transform: translateY(-2px); } +.add-btn { + background: #48bb78 !important; +} + +.add-btn:hover { + background: #38a169 !important; +} + +/* 파일 추가 섹션 스타일 */ +.add-section { + background: rgba(255, 255, 255, 0.95); + padding: 30px; + border-radius: 15px; + margin-bottom: 30px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); +} + +.form-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; +} + +.form-header h2 { + color: #4a5568; + margin: 0; + font-size: 1.8rem; +} + +.toggle-btn { + padding: 8px 16px; + background: #e53e3e; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.toggle-btn:hover { + background: #c53030; +} + +.add-form { + display: grid; + gap: 20px; +} + +.form-actions { + display: flex; + gap: 15px; + margin-top: 10px; +} + +.submit-btn { + flex: 1; + padding: 12px 20px; + background: #48bb78; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.submit-btn:hover { + background: #38a169; + transform: translateY(-1px); +} + +.cancel-btn { + padding: 12px 20px; + background: #e2e8f0; + color: #4a5568; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.cancel-btn:hover { + background: #cbd5e0; +} + .form-section { background: rgba(255, 255, 255, 0.95); padding: 30px; @@ -466,6 +655,34 @@ header p { color: #10b981; } +/* 일반 사용자 페이지 첨부파일 목록 - 관리자와 동일한 스타일 */ +.attachment-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 120px; + overflow-y: auto; +} + +.attachment-item-public { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + background: #f8fafc; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid #e2e8f0; +} + +.attachment-item-public:hover { + background: #e2e8f0; + border-color: #cbd5e1; + transform: translateY(-1px); +} + +/* 기존 스타일 유지 (하위 호환성) */ .attachment-icons { display: flex; flex-direction: column; @@ -495,11 +712,10 @@ header p { } .attachment-file-icon { - font-size: 1.1rem; - width: auto; - text-align: left; + font-size: 0.9rem; + min-width: 16px; + text-align: center; flex-shrink: 0; - margin-right: 1px; } .attachment-file-info { @@ -512,12 +728,13 @@ header p { } .attachment-file-name { - font-weight: 500; - color: #374151; + flex: 1; + font-size: 0.75rem; + color: #475569; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - font-size: 0.8rem; + max-width: 120px; } .attachment-file-size { diff --git a/synology-setup.md b/synology-setup.md new file mode 100644 index 0000000..f5722ce --- /dev/null +++ b/synology-setup.md @@ -0,0 +1,248 @@ +# 시놀로지 NAS에서 Jaryo File Manager 서비스 실행 가이드 + +## 1. 사전 준비사항 + +### 1.1 DSM 패키지 설치 +1. **DSM 제어판** → **패키지 센터** 접속 +2. 다음 패키지들을 설치: + - **Node.js** (최신 LTS 버전 권장) + - **Git Server** (선택사항, 소스코드 관리용) + - **Web Station** (선택사항, 웹 서버 프록시용) + +### 1.2 SSH 활성화 +1. **DSM 제어판** → **터미널 및 SNMP** → **SSH 서비스 활성화** +2. 포트 번호 확인 (기본: 22) + +## 2. 프로젝트 배포 + +### 2.1 방법 1: 직접 파일 업로드 (간단한 방법) + +1. **File Station**에서 `/volume1/web/` 폴더 생성 +2. 프로젝트 파일들을 `jaryo` 폴더에 업로드 +3. SSH로 접속하여 설정 + +### 2.2 방법 2: Git을 통한 배포 (권장) + +```bash +# NAS에 SSH 접속 +ssh admin@your-nas-ip + +# 프로젝트 디렉토리 생성 +mkdir -p /volume1/web/jaryo +cd /volume1/web/jaryo + +# Git 저장소 클론 (로컬에서 push한 경우) +git clone [your-repository-url] . + +# 또는 로컬에서 직접 파일 복사 +# scp -r ./jaryo/* admin@your-nas-ip:/volume1/web/jaryo/ +``` + +## 3. 서비스 설정 및 실행 + +### 3.1 스크립트 권한 설정 + +```bash +# SSH로 NAS 접속 +ssh admin@your-nas-ip + +# 프로젝트 디렉토리로 이동 +cd /volume1/web/jaryo + +# 스크립트 실행 권한 부여 +chmod +x start-service.sh +chmod +x stop-service.sh +``` + +### 3.2 서비스 시작 + +```bash +# 서비스 시작 +./start-service.sh + +# 로그 확인 +tail -f logs/app.log + +# 프로세스 상태 확인 +ps aux | grep "node server.js" +``` + +### 3.3 서비스 중지 + +```bash +# 서비스 중지 +./stop-service.sh +``` + +## 4. 자동 시작 설정 (선택사항) + +### 4.1 Task Scheduler 사용 + +1. **DSM 제어판** → **작업 스케줄러** +2. **작업 생성** → **사용자 정의 스크립트** +3. 설정: + - **작업 이름**: Jaryo Service Start + - **사용자**: root + - **스케줄**: 시스템 부팅 시 + - **작업 설정**: `/volume1/web/jaryo/start-service.sh` + +### 4.2 rc.local 사용 (고급 사용자) + +```bash +# /etc/rc.local 파일 편집 +sudo vi /etc/rc.local + +# 다음 라인 추가 +/volume1/web/jaryo/start-service.sh & + +# 파일 저장 후 권한 설정 +chmod +x /etc/rc.local +``` + +## 5. 방화벽 및 포트 설정 + +### 5.1 DSM 방화벽 설정 + +1. **DSM 제어판** → **보안** → **방화벽** +2. **방화벽 규칙 편집** → **규칙 생성** +3. 설정: + - **포트**: 3005 (애플리케이션 포트) + - **프로토콜**: TCP + - **소스**: 허용할 IP 범위 + +### 5.2 라우터 포트 포워딩 (외부 접속용) + +라우터에서 포트 3005를 NAS의 IP로 포워딩 설정 + +## 6. 웹 서버 프록시 설정 (Web Station 사용) + +### 6.1 Web Station 설정 + +1. **Web Station** → **가상 호스트** → **생성** +2. 설정: + - **도메인 이름**: your-domain.com (또는 IP) + - **포트**: 80 (또는 443 for HTTPS) + - **문서 루트**: `/volume1/web/jaryo` + - **HTTP 백엔드 서버**: `http://localhost:3005` + +### 6.2 Apache 설정 (고급) + +```apache +# /volume1/web/apache/conf/vhost/VirtualHost.conf + + ServerName your-domain.com + DocumentRoot /volume1/web/jaryo + + ProxyPreserveHost On + ProxyPass / http://localhost:3005/ + ProxyPassReverse / http://localhost:3005/ + + + AllowOverride All + Require all granted + + +``` + +## 7. 모니터링 및 유지보수 + +### 7.1 로그 모니터링 + +```bash +# 실시간 로그 확인 +tail -f /volume1/web/jaryo/logs/app.log + +# 로그 파일 크기 확인 +du -h /volume1/web/jaryo/logs/app.log + +# 로그 로테이션 설정 (logrotate 사용) +``` + +### 7.2 서비스 상태 확인 + +```bash +# 프로세스 확인 +ps aux | grep "node server.js" + +# 포트 사용 확인 +netstat -tlnp | grep :3005 + +# 메모리 사용량 확인 +top -p $(cat /volume1/web/jaryo/app.pid) +``` + +### 7.3 백업 설정 + +1. **Hyper Backup** 패키지 설치 +2. `/volume1/web/jaryo` 폴더 백업 스케줄 설정 +3. 데이터베이스 파일 (`jaryo.db`) 별도 백업 권장 + +## 8. 문제 해결 + +### 8.1 일반적인 문제들 + +**서비스가 시작되지 않는 경우:** +```bash +# Node.js 설치 확인 +which node +node --version + +# 의존성 재설치 +cd /volume1/web/jaryo +rm -rf node_modules package-lock.json +npm install + +# 권한 문제 확인 +ls -la /volume1/web/jaryo/ +chown -R admin:users /volume1/web/jaryo/ +``` + +**포트 충돌 문제:** +```bash +# 포트 사용 확인 +netstat -tlnp | grep :3005 + +# 다른 포트로 변경 (server.js 수정) +# const PORT = process.env.PORT || 3006; +``` + +**메모리 부족 문제:** +```bash +# 메모리 사용량 확인 +free -h + +# Node.js 메모리 제한 설정 +# node --max-old-space-size=512 server.js +``` + +### 8.2 로그 분석 + +```bash +# 에러 로그만 확인 +grep -i error /volume1/web/jaryo/logs/app.log + +# 최근 100줄 확인 +tail -100 /volume1/web/jaryo/logs/app.log + +# 특정 시간대 로그 확인 +grep "2024-01-15" /volume1/web/jaryo/logs/app.log +``` + +## 9. 보안 고려사항 + +1. **HTTPS 설정**: Let's Encrypt 인증서 사용 +2. **방화벽 강화**: 필요한 포트만 개방 +3. **정기 업데이트**: Node.js 및 패키지 업데이트 +4. **백업**: 정기적인 데이터 백업 +5. **모니터링**: 로그 모니터링 및 알림 설정 + +## 10. 성능 최적화 + +1. **PM2 사용**: 프로세스 관리자로 PM2 사용 고려 +2. **캐싱**: 정적 파일 캐싱 설정 +3. **압축**: gzip 압축 활성화 +4. **CDN**: 정적 파일 CDN 사용 고려 + +--- + +**참고**: 이 가이드는 시놀로지 DSM 7.x 기준으로 작성되었습니다. 버전에 따라 일부 설정이 다를 수 있습니다. diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 0000000..2b2a56d --- /dev/null +++ b/uploads/.gitkeep @@ -0,0 +1,2 @@ +# This file ensures the uploads directory is tracked by Git +# Uploaded files will be ignored by .gitignore, but the directory structure is preserved