Enhance pagination and UI consistency across admin/user interfaces

- Fixed pagination to display exactly 10 items per page
- Made pagination controls always visible, even with empty data
- Synchronized data structure and sorting between admin and main pages
- Improved pagination styling with better visibility and centering
- Enhanced attachment display with file icons, names, and sizes
- Implemented detailed view pages for both interfaces
- Optimized table row spacing for more compact display
- Centered attachment icons with file names for better visual balance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-19 20:29:32 +09:00
parent 7a08bf9b4c
commit 122d0e2582
4 changed files with 1004 additions and 184 deletions

179
script.js
View File

@@ -65,7 +65,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 +74,7 @@ class FileManager {
return a.category.localeCompare(b.category);
case 'date':
default:
return new Date(b.createdAt) - new Date(a.createdAt);
return new Date(b.created_at || b.createdAt) - new Date(a.created_at || a.createdAt);
}
});
@@ -118,9 +118,15 @@ class FileManager {
</td>
<td class="col-attachment">
${hasAttachments ?
`<div class="attachment-icons" title="${file.files.length}개 파일">${file.files.map((f, index) =>
`<span class="attachment-icon-clickable" onclick="fileManager.downloadSingleFile('${file.id}', ${index})" title="${f.name || f.original_name || '파일'}">${this.getFileIcon(f.name || f.original_name || 'unknown')}</span>`
).join(' ')}</div>` :
`<div class="attachment-icons">${file.files.map((f, index) =>
`<div class="attachment-file-item" onclick="fileManager.downloadSingleFile('${file.id}', ${index})" title="클릭하여 다운로드">
<span class="attachment-file-icon">${this.getFileIcon(f.name || f.original_name || 'unknown')}</span>
<div class="attachment-file-info">
<div class="attachment-file-name">${this.escapeHtml(f.name || f.original_name || '파일')}</div>
<div class="attachment-file-size">${this.formatFileSize(f.size || 0)}</div>
</div>
</div>`
).join('')}</div>` :
`<span class="no-attachment">-</span>`
}
</td>
@@ -151,25 +157,132 @@ class FileManager {
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;
let info = `📋 자료 정보\n\n`;
info += `📌 제목: ${file.title}\n`;
info += `📂 카테고리: ${file.category}\n`;
info += `📅 등록일: ${new Date(file.created_at || file.createdAt).toLocaleDateString('ko-KR')}\n`;
if (file.description) info += `📝 설명: ${file.description}\n`;
if (file.tags && file.tags.length > 0) info += `🏷️ 태그: ${file.tags.join(', ')}\n`;
if (file.files && file.files.length > 0) {
info += `\n📎 첨부파일 (${file.files.length}개):\n`;
file.files.forEach((attachment, index) => {
const icon = this.getFileIcon(attachment.name || attachment.original_name || 'unknown');
info += ` ${index + 1}. ${icon} ${attachment.name || attachment.original_name || '파일'}\n`;
});
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 = `
<div class="container">
<header>
<h1>📋 자료 상세보기</h1>
<p>등록된 자료의 상세 정보를 확인하세요</p>
</header>
<div class="detail-section">
<div class="detail-header">
<h2>📄 ${this.escapeHtml(file.title)}</h2>
<button class="back-btn" onclick="fileManager.hideDetailView()">
← 목록으로 돌아가기
</button>
</div>
<div class="detail-content">
<div class="detail-info">
<div class="info-group">
<label>📂 카테고리</label>
<div class="info-value">
<span class="category-badge category-${file.category}">${file.category}</span>
</div>
</div>
<div class="info-group">
<label>📝 설명</label>
<div class="info-value description">
${file.description ? this.escapeHtml(file.description) : '설명이 없습니다.'}
</div>
</div>
${file.tags && file.tags.length > 0 ? `
<div class="info-group">
<label>🏷️ 태그</label>
<div class="info-value">
<div class="tags-container">
${file.tags.map(tag => `<span class="tag">#${this.escapeHtml(tag)}</span>`).join('')}
</div>
</div>
</div>` : ''}
${file.files && file.files.length > 0 ? `
<div class="info-group">
<label>📎 첨부파일 (${file.files.length}개)</label>
<div class="info-value">
<div class="attachments-list">
${file.files.map((f, index) => `
<div class="attachment-item">
<span class="attachment-icon">${this.getFileIcon(f.name || f.original_name || 'unknown')}</span>
<span class="attachment-name">${this.escapeHtml(f.name || f.original_name || '파일')}</span>
<button class="download-single-btn" onclick="fileManager.downloadSingleFile('${file.id}', ${index})" title="다운로드">
📥 다운로드
</button>
</div>
`).join('')}
</div>
<div class="attachment-actions">
<button class="download-all-btn" onclick="fileManager.downloadFiles('${file.id}')" title="모든 파일 다운로드">
📦 모든 파일 다운로드
</button>
</div>
</div>
</div>` : `
<div class="info-group">
<label>📎 첨부파일</label>
<div class="info-value no-files">
첨부된 파일이 없습니다.
</div>
</div>`}
<div class="info-group">
<label>📅 등록일</label>
<div class="info-value">${createdDate}</div>
</div>
${createdDate !== updatedDate ? `
<div class="info-group">
<label>🔄 수정일</label>
<div class="info-value">${updatedDate}</div>
</div>` : ''}
</div>
</div>
</div>
</div>
`;
document.body.appendChild(detailContainer);
}
hideDetailView() {
const detailContainer = document.getElementById('detailContainer');
if (detailContainer) {
detailContainer.remove();
}
alert(info);
// 메인 컨테이너 다시 보이기
const container = document.querySelector('.container');
container.style.display = 'block';
}
async downloadFiles(id) {
@@ -262,20 +375,23 @@ class FileManager {
}
updatePagination() {
const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage);
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 (totalPages <= 1) {
pagination.style.display = 'none';
} else {
pagination.style.display = 'flex';
prevBtn.disabled = this.currentPage <= 1;
nextBtn.disabled = this.currentPage >= totalPages;
pageInfo.textContent = `${this.currentPage} / ${totalPages}`;
}
// 항상 페이지네이션을 표시
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() {
@@ -299,7 +415,14 @@ class FileManager {
loadFiles() {
try {
const stored = localStorage.getItem('fileManagerData');
return stored ? JSON.parse(stored) : [];
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 [];