class PublicFileViewer { constructor() { this.files = []; this.currentPage = 1; this.itemsPerPage = 10; this.filteredFiles = []; this.init(); } async init() { console.log('πŸš€ PublicFileViewer μ΄ˆκΈ°ν™” μ‹œμž‘'); try { this.showLoading(true); console.log('πŸ“‘ 파일 λͺ©λ‘ λ‘œλ“œ 쀑...'); await this.loadFiles(); this.filteredFiles = [...this.files]; console.log(`βœ… ${this.files.length}개 파일 λ‘œλ“œ μ™„λ£Œ`); 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(); }); document.getElementById('categoryFilter').addEventListener('change', () => this.handleSearch()); document.getElementById('sortBy').addEventListener('change', () => this.handleSearch()); // νŽ˜μ΄μ§€λ„€μ΄μ…˜ 이벀트 document.getElementById('prevPage').addEventListener('click', () => this.goToPrevPage()); document.getElementById('nextPage').addEventListener('click', () => this.goToNextPage()); } 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() { const searchTerm = document.getElementById('searchInput').value.toLowerCase(); const 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); } this.filteredFiles = filteredFiles; this.currentPage = 1; // 검색 μ‹œ 첫 νŽ˜μ΄μ§€λ‘œ 리셋 this.renderFiles(); this.updatePagination(); } renderFiles() { const fileList = document.getElementById('fileList'); const sortBy = document.getElementById('sortBy').value; // μ •λ ¬ 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; } fileList.innerHTML = paginatedFiles.map((file, index) => this.createFileRowHTML(file, startIndex + index + 1) ).join(''); } createFileRowHTML(file, rowNumber) { const createdDate = new Date(file.created_at).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)}` : ''} ${file.tags && file.tags.length > 0 ? `
${this.parseJsonTags(file.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('')}
` : `-` } ${createdDate} ${hasAttachments ? `` : `-` } `; } 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 = { 'pdf': 'πŸ“„', 'doc': 'πŸ“', 'docx': 'πŸ“', 'xls': 'πŸ“Š', 'xlsx': 'πŸ“Š', 'ppt': 'πŸ“½οΈ', 'pptx': 'πŸ“½οΈ', 'jpg': 'πŸ–ΌοΈ', 'jpeg': 'πŸ–ΌοΈ', 'png': 'πŸ–ΌοΈ', 'gif': 'πŸ–ΌοΈ', 'mp4': 'πŸŽ₯', 'avi': 'πŸŽ₯', 'mov': 'πŸŽ₯', 'mp3': '🎡', 'wav': '🎡', 'zip': 'πŸ“¦', 'rar': 'πŸ“¦', 'txt': 'πŸ“„' }; return iconMap[ext] || 'πŸ“„'; } 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]; } viewFileInfo(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).toLocaleDateString('ko-KR'); const updatedDate = new Date(file.updated_at).toLocaleDateString('ko-KR'); const tags = this.parseJsonTags(file.tags); detailContainer.innerHTML = `

πŸ“‹ 자료 상세보기

λ“±λ‘λœ 자료의 상세 정보λ₯Ό ν™•μΈν•˜μ„Έμš”

πŸ“„ ${this.escapeHtml(file.title)}

${file.category}
${file.description ? this.escapeHtml(file.description) : 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€.'}
${tags && tags.length > 0 ? `
${tags.map(tag => `#${this.escapeHtml(tag)}`).join('')}
` : ''} ${file.files && file.files.length > 0 ? `
${file.files.map((f, index) => `
${this.getFileIcon(f.original_name || 'unknown')} ${this.escapeHtml(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'; } async downloadFiles(id) { const file = this.files.find(f => f.id === id); if (!file || !file.files || file.files.length === 0) { this.showNotification('μ²¨λΆ€νŒŒμΌμ΄ μ—†μŠ΅λ‹ˆλ‹€.', 'error'); return; } try { if (file.files.length === 1) { // 단일 파일: 직접 λ‹€μš΄λ‘œλ“œ await this.downloadSingleFile(id, 0); } else { // 닀쀑 파일: 각각 λ‹€μš΄λ‘œλ“œ 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'); } } catch (error) { console.error('파일 λ‹€μš΄λ‘œλ“œ 였λ₯˜:', error); this.showNotification('파일 λ‹€μš΄λ‘œλ“œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', 'error'); } } async downloadSingleFile(fileId, attachmentIndex) { try { 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); // λŒ€μš©λŸ‰ νŒŒμΌμ„ μœ„ν•΄ 직접 링크둜 λ‹€μš΄λ‘œλ“œ (blob μ‚¬μš©ν•˜μ§€ μ•ŠμŒ) const link = document.createElement('a'); link.href = downloadUrl; link.target = '_blank'; // μƒˆ νƒ­μ—μ„œ μ—΄μ–΄ λ‹€μš΄λ‘œλ“œ // 파일λͺ… μ„€μ • (μ„œλ²„μ—μ„œ Content-Disposition ν—€λ”λ‘œ 처리됨) const filename = file.files[attachmentIndex].original_name || `download_${Date.now()}`; link.download = filename; // μˆ¨κ²¨μ§„ 링크 생성 ν›„ 클릭 link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log('πŸ“ λŒ€μš©λŸ‰ 파일 λ‹€μš΄λ‘œλ“œ μ‹œμž‘:', filename); if (arguments.length === 2) { // 단일 파일 λ‹€μš΄λ‘œλ“œμΈ 경우만 μ•Œλ¦Ό ν‘œμ‹œ this.showNotification(`파일 λ‹€μš΄λ‘œλ“œ μ‹œμž‘: ${filename}`, 'success'); } } catch (error) { console.error('downloadSingleFile 였λ₯˜:', error); this.showNotification('파일 λ‹€μš΄λ‘œλ“œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', 'error'); } } 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; 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); setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, 3000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // μ „μ—­ μΈμŠ€ν„΄μŠ€ 생성 let publicViewer; document.addEventListener('DOMContentLoaded', () => { publicViewer = new PublicFileViewer(); });