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:
269
admin/script.js
269
admin/script.js
@@ -975,28 +975,44 @@ class FileManager {
|
|||||||
const tbody = document.getElementById('fileList');
|
const tbody = document.getElementById('fileList');
|
||||||
const sortBy = document.getElementById('sortBy').value;
|
const sortBy = document.getElementById('sortBy').value;
|
||||||
|
|
||||||
let sortedFiles = [...this.files];
|
// 검색 및 필터 적용 (메인 페이지와 동일하게)
|
||||||
|
const searchTerm = document.getElementById('searchInput') ? document.getElementById('searchInput').value.toLowerCase().trim() : '';
|
||||||
|
const categoryFilter = document.getElementById('categoryFilter') ? document.getElementById('categoryFilter').value : '';
|
||||||
|
|
||||||
switch (sortBy) {
|
let filteredFiles = [...this.files];
|
||||||
case 'title':
|
|
||||||
sortedFiles.sort((a, b) => a.title.localeCompare(b.title));
|
if (searchTerm) {
|
||||||
break;
|
filteredFiles = filteredFiles.filter(file =>
|
||||||
case 'category':
|
file.title.toLowerCase().includes(searchTerm) ||
|
||||||
sortedFiles.sort((a, b) => a.category.localeCompare(b.category));
|
file.description.toLowerCase().includes(searchTerm) ||
|
||||||
break;
|
(file.tags && file.tags.some(tag => tag.toLowerCase().includes(searchTerm)))
|
||||||
case 'date':
|
);
|
||||||
default:
|
|
||||||
sortedFiles.sort((a, b) => new Date(b.created_at || b.createdAt) - new Date(a.created_at || a.createdAt));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allFiles = sortedFiles; // 전체 파일 목록 저장
|
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();
|
this.updatePagination();
|
||||||
|
|
||||||
if (sortedFiles.length === 0) {
|
if (sortedFiles.length === 0) {
|
||||||
tbody.innerHTML = `
|
tbody.innerHTML = `
|
||||||
<tr class="empty-state">
|
<tr class="empty-state">
|
||||||
<td colspan="6">📂 등록된 자료가 없습니다. 새 자료를 추가해보세요!</td>
|
<td colspan="6">📂 조건에 맞는 자료가 없습니다.</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -1026,7 +1042,7 @@ class FileManager {
|
|||||||
</td>
|
</td>
|
||||||
<td class="col-title">
|
<td class="col-title">
|
||||||
<div class="board-title" onclick="fileManager.viewFile('${file.id}')">
|
<div class="board-title" onclick="fileManager.viewFile('${file.id}')">
|
||||||
<strong>${this.escapeHtml(file.title)}</strong>
|
${this.escapeHtml(file.title)}
|
||||||
${file.description ? `<br><small style="color: #6b7280; font-weight: normal;">${this.escapeHtml(file.description.substring(0, 80))}${file.description.length > 80 ? '...' : ''}</small>` : ''}
|
${file.description ? `<br><small style="color: #6b7280; font-weight: normal;">${this.escapeHtml(file.description.substring(0, 80))}${file.description.length > 80 ? '...' : ''}</small>` : ''}
|
||||||
${file.tags && file.tags.length > 0 ?
|
${file.tags && file.tags.length > 0 ?
|
||||||
`<br><div style="margin-top: 4px;">${file.tags.map(tag => `<span style="display: inline-block; background: #e5e7eb; color: #374151; padding: 2px 6px; border-radius: 10px; font-size: 0.7rem; margin-right: 4px;">#${this.escapeHtml(tag)}</span>`).join('')}</div>` : ''
|
`<br><div style="margin-top: 4px;">${file.tags.map(tag => `<span style="display: inline-block; background: #e5e7eb; color: #374151; padding: 2px 6px; border-radius: 10px; font-size: 0.7rem; margin-right: 4px;">#${this.escapeHtml(tag)}</span>`).join('')}</div>` : ''
|
||||||
@@ -1035,9 +1051,15 @@ class FileManager {
|
|||||||
</td>
|
</td>
|
||||||
<td class="col-attachment">
|
<td class="col-attachment">
|
||||||
${hasAttachments ?
|
${hasAttachments ?
|
||||||
`<div class="attachment-icons" title="${file.files.length}개 파일">${file.files.map((f, index) =>
|
`<div class="attachment-icons">${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>`
|
`<div class="attachment-file-item" onclick="fileManager.downloadSingleFile('${file.id}', ${index})" title="클릭하여 다운로드">
|
||||||
).join(' ')}</div>` :
|
<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>`
|
`<span class="no-attachment">-</span>`
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
@@ -1104,23 +1126,23 @@ class FileManager {
|
|||||||
|
|
||||||
// 페이지네이션 관련 함수들
|
// 페이지네이션 관련 함수들
|
||||||
updatePagination() {
|
updatePagination() {
|
||||||
|
const totalPages = Math.max(1, Math.ceil(this.allFiles.length / 10));
|
||||||
const pagination = document.getElementById('pagination');
|
const pagination = document.getElementById('pagination');
|
||||||
const prevBtn = document.getElementById('prevPage');
|
const prevBtn = document.getElementById('prevPage');
|
||||||
const nextBtn = document.getElementById('nextPage');
|
const nextBtn = document.getElementById('nextPage');
|
||||||
const pageInfo = document.getElementById('pageInfo');
|
const pageInfo = document.getElementById('pageInfo');
|
||||||
|
|
||||||
const totalFiles = this.allFiles.length;
|
// 항상 페이지네이션을 표시
|
||||||
const itemsPerPage = 10;
|
if (pagination) pagination.style.display = 'flex';
|
||||||
const totalPages = Math.ceil(totalFiles / itemsPerPage);
|
|
||||||
|
|
||||||
if (totalPages <= 1) {
|
// 페이지 버튼 상태 업데이트
|
||||||
pagination.style.display = 'none';
|
if (prevBtn) prevBtn.disabled = this.currentPage <= 1;
|
||||||
} else {
|
if (nextBtn) nextBtn.disabled = this.currentPage >= totalPages || this.allFiles.length === 0;
|
||||||
pagination.style.display = 'flex';
|
|
||||||
prevBtn.disabled = this.currentPage <= 1;
|
// 페이지 정보 표시 (아이템이 없어도 1/1로 표시)
|
||||||
nextBtn.disabled = this.currentPage >= totalPages;
|
const displayTotalPages = this.allFiles.length === 0 ? 1 : totalPages;
|
||||||
pageInfo.textContent = `${this.currentPage} / ${totalPages}`;
|
const displayCurrentPage = this.allFiles.length === 0 ? 1 : this.currentPage;
|
||||||
}
|
if (pageInfo) pageInfo.textContent = `${displayCurrentPage} / ${displayTotalPages}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPrevPage() {
|
goToPrevPage() {
|
||||||
@@ -1146,21 +1168,140 @@ class FileManager {
|
|||||||
const file = this.files.find(f => f.id === id);
|
const file = this.files.find(f => f.id === id);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
// 간단한 알림으로 파일 정보 표시
|
this.showDetailView(file);
|
||||||
let info = `📄 ${file.title}\n\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`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(info);
|
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>
|
||||||
|
<div class="detail-actions">
|
||||||
|
<button class="edit-detail-btn" onclick="fileManager.editFileFromDetail('${file.id}')" title="수정">
|
||||||
|
✏️ 수정
|
||||||
|
</button>
|
||||||
|
<button class="delete-detail-btn" onclick="fileManager.deleteFileFromDetail('${file.id}')" title="삭제">
|
||||||
|
🗑️ 삭제
|
||||||
|
</button>
|
||||||
|
<button class="back-btn" onclick="fileManager.hideDetailView()">
|
||||||
|
← 목록으로 돌아가기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메인 컨테이너 다시 보이기
|
||||||
|
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) {
|
editFile(id) {
|
||||||
@@ -1331,49 +1472,11 @@ class FileManager {
|
|||||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
|
||||||
const categoryFilter = document.getElementById('categoryFilter').value;
|
const categoryFilter = document.getElementById('categoryFilter').value;
|
||||||
|
|
||||||
let filteredFiles = this.files;
|
// 검색 시 첫 페이지로 리셋
|
||||||
|
this.currentPage = 1;
|
||||||
if (searchTerm) {
|
this.renderFiles();
|
||||||
filteredFiles = filteredFiles.filter(file =>
|
|
||||||
file.title.toLowerCase().includes(searchTerm) ||
|
|
||||||
file.description.toLowerCase().includes(searchTerm) ||
|
|
||||||
file.tags.some(tag => tag.toLowerCase().includes(searchTerm))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryFilter) {
|
|
||||||
filteredFiles = filteredFiles.filter(file => file.category === categoryFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderFilteredFiles(filteredFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFilteredFiles(files) {
|
|
||||||
const container = document.getElementById('fileList');
|
|
||||||
const sortBy = document.getElementById('sortBy').value;
|
|
||||||
|
|
||||||
let sortedFiles = [...files];
|
|
||||||
|
|
||||||
switch (sortBy) {
|
|
||||||
case 'title':
|
|
||||||
sortedFiles.sort((a, b) => a.title.localeCompare(b.title));
|
|
||||||
break;
|
|
||||||
case 'category':
|
|
||||||
sortedFiles.sort((a, b) => a.category.localeCompare(b.category));
|
|
||||||
break;
|
|
||||||
case 'date':
|
|
||||||
default:
|
|
||||||
sortedFiles.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortedFiles.length === 0) {
|
|
||||||
container.innerHTML = '<div class="empty-state"><p>🔍 검색 결과가 없습니다. 다른 키워드로 검색해보세요!</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = sortedFiles.map(file => this.createFileHTML(file)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
clearForm() {
|
clearForm() {
|
||||||
document.getElementById('fileForm').reset();
|
document.getElementById('fileForm').reset();
|
||||||
|
364
admin/styles.css
364
admin/styles.css
@@ -385,7 +385,7 @@ header p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.board-table th {
|
.board-table th {
|
||||||
padding: 15px 12px;
|
padding: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
@@ -410,17 +410,18 @@ header p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.board-table td {
|
.board-table td {
|
||||||
padding: 12px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 컬럼 너비 설정 */
|
/* 컬럼 너비 설정 */
|
||||||
.col-no { width: 60px; }
|
.col-no { width: 60px; }
|
||||||
.col-category { width: 100px; }
|
.col-category { width: 100px; }
|
||||||
.col-title { width: auto; min-width: 200px; text-align: left; }
|
.col-title { width: auto; min-width: 200px; text-align: left; }
|
||||||
.col-attachment { width: 80px; }
|
.col-attachment { width: 220px; text-align: center; }
|
||||||
.col-date { width: 120px; }
|
.col-date { width: 120px; }
|
||||||
.col-actions { width: 150px; }
|
.col-actions { width: 150px; }
|
||||||
|
|
||||||
@@ -431,7 +432,7 @@ header p {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px;
|
padding: 4px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -465,16 +466,61 @@ header p {
|
|||||||
|
|
||||||
.attachment-icons {
|
.attachment-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
flex-direction: column;
|
||||||
justify-content: center;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
font-size: 0.85rem;
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-icons span {
|
.attachment-file-item {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-item:hover {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-size {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-icon-clickable {
|
.attachment-icon-clickable {
|
||||||
@@ -496,15 +542,15 @@ header p {
|
|||||||
/* 액션 버튼 */
|
/* 액션 버튼 */
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 3px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 6px 12px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -549,6 +595,298 @@ header p {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 상세보기 페이지 스타일 */
|
||||||
|
.detail-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-detail-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-detail-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-detail-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-detail-btn:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #5a67d8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info {
|
||||||
|
display: grid;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.description {
|
||||||
|
min-height: 80px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.no-files {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachments-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-single-btn {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-single-btn:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-actions {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-all-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-all-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모바일 반응형 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-section {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-single-btn {
|
||||||
|
align-self: stretch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 파일 업로드 영역 */
|
/* 파일 업로드 영역 */
|
||||||
.file-upload-area {
|
.file-upload-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
175
script.js
175
script.js
@@ -65,7 +65,7 @@ class FileManager {
|
|||||||
const fileList = document.getElementById('fileList');
|
const fileList = document.getElementById('fileList');
|
||||||
const sortBy = document.getElementById('sortBy').value;
|
const sortBy = document.getElementById('sortBy').value;
|
||||||
|
|
||||||
// 정렬
|
// 정렬 (관리자 페이지와 동일하게)
|
||||||
const sortedFiles = [...this.filteredFiles].sort((a, b) => {
|
const sortedFiles = [...this.filteredFiles].sort((a, b) => {
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'title':
|
case 'title':
|
||||||
@@ -74,7 +74,7 @@ class FileManager {
|
|||||||
return a.category.localeCompare(b.category);
|
return a.category.localeCompare(b.category);
|
||||||
case 'date':
|
case 'date':
|
||||||
default:
|
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>
|
||||||
<td class="col-attachment">
|
<td class="col-attachment">
|
||||||
${hasAttachments ?
|
${hasAttachments ?
|
||||||
`<div class="attachment-icons" title="${file.files.length}개 파일">${file.files.map((f, index) =>
|
`<div class="attachment-icons">${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>`
|
`<div class="attachment-file-item" onclick="fileManager.downloadSingleFile('${file.id}', ${index})" title="클릭하여 다운로드">
|
||||||
).join(' ')}</div>` :
|
<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>`
|
`<span class="no-attachment">-</span>`
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
@@ -151,25 +157,132 @@ class FileManager {
|
|||||||
return iconMap[ext] || '📄';
|
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) {
|
viewFileInfo(id) {
|
||||||
const file = this.files.find(f => f.id === id);
|
const file = this.files.find(f => f.id === id);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
let info = `📋 자료 정보\n\n`;
|
this.showDetailView(file);
|
||||||
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`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(info);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메인 컨테이너 다시 보이기
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
container.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFiles(id) {
|
async downloadFiles(id) {
|
||||||
@@ -262,20 +375,23 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePagination() {
|
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 pagination = document.getElementById('pagination');
|
||||||
const prevBtn = document.getElementById('prevPage');
|
const prevBtn = document.getElementById('prevPage');
|
||||||
const nextBtn = document.getElementById('nextPage');
|
const nextBtn = document.getElementById('nextPage');
|
||||||
const pageInfo = document.getElementById('pageInfo');
|
const pageInfo = document.getElementById('pageInfo');
|
||||||
|
|
||||||
if (totalPages <= 1) {
|
// 항상 페이지네이션을 표시
|
||||||
pagination.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
pagination.style.display = 'flex';
|
pagination.style.display = 'flex';
|
||||||
|
|
||||||
|
// 페이지 버튼 상태 업데이트
|
||||||
prevBtn.disabled = this.currentPage <= 1;
|
prevBtn.disabled = this.currentPage <= 1;
|
||||||
nextBtn.disabled = this.currentPage >= totalPages;
|
nextBtn.disabled = this.currentPage >= totalPages || this.filteredFiles.length === 0;
|
||||||
pageInfo.textContent = `${this.currentPage} / ${totalPages}`;
|
|
||||||
}
|
// 페이지 정보 표시 (아이템이 없어도 1/1로 표시)
|
||||||
|
const displayTotalPages = this.filteredFiles.length === 0 ? 1 : totalPages;
|
||||||
|
const displayCurrentPage = this.filteredFiles.length === 0 ? 1 : this.currentPage;
|
||||||
|
pageInfo.textContent = `${displayCurrentPage} / ${displayTotalPages}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPrevPage() {
|
goToPrevPage() {
|
||||||
@@ -299,7 +415,14 @@ class FileManager {
|
|||||||
loadFiles() {
|
loadFiles() {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('fileManagerData');
|
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) {
|
} catch (error) {
|
||||||
console.error('파일 로드 중 오류:', error);
|
console.error('파일 로드 중 오류:', error);
|
||||||
return [];
|
return [];
|
||||||
|
376
styles.css
376
styles.css
@@ -40,45 +40,6 @@ header p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 페이지네이션 스타일 */
|
/* 페이지네이션 스타일 */
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 20px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-btn {
|
|
||||||
background: #4299e1;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-btn:hover:not(:disabled) {
|
|
||||||
background: #3182ce;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-btn:disabled {
|
|
||||||
background: #a0aec0;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pageInfo {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4a5568;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-section {
|
.auth-section {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -426,7 +387,7 @@ header p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.board-table th {
|
.board-table th {
|
||||||
padding: 15px 12px;
|
padding: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
@@ -451,17 +412,18 @@ header p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.board-table td {
|
.board-table td {
|
||||||
padding: 12px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 컬럼 너비 설정 */
|
/* 컬럼 너비 설정 */
|
||||||
.col-no { width: 60px; }
|
.col-no { width: 60px; }
|
||||||
.col-category { width: 100px; }
|
.col-category { width: 100px; }
|
||||||
.col-title { width: auto; min-width: 200px; text-align: left; }
|
.col-title { width: auto; min-width: 200px; text-align: left; }
|
||||||
.col-attachment { width: 80px; }
|
.col-attachment { width: 220px; text-align: center; }
|
||||||
.col-date { width: 120px; }
|
.col-date { width: 120px; }
|
||||||
.col-actions { width: 150px; }
|
.col-actions { width: 150px; }
|
||||||
|
|
||||||
@@ -472,7 +434,7 @@ header p {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px;
|
padding: 4px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -506,16 +468,61 @@ header p {
|
|||||||
|
|
||||||
.attachment-icons {
|
.attachment-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
flex-direction: column;
|
||||||
justify-content: center;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
font-size: 0.85rem;
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-icons span {
|
.attachment-file-item {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-item:hover {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file-size {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-icon-clickable {
|
.attachment-icon-clickable {
|
||||||
@@ -537,15 +544,15 @@ header p {
|
|||||||
/* 액션 버튼 */
|
/* 액션 버튼 */
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 3px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 6px 12px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -575,39 +582,288 @@ header p {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 20px;
|
||||||
margin-top: 20px;
|
margin-top: 30px;
|
||||||
padding: 20px 0;
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-btn {
|
.page-btn {
|
||||||
padding: 8px 16px;
|
padding: 12px 20px;
|
||||||
border: 2px solid #e2e8f0;
|
border: 2px solid #e2e8f0;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: white;
|
background: white;
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.3s ease;
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-btn:hover:not(:disabled) {
|
.page-btn:hover:not(:disabled) {
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
background: #f0f4ff;
|
background: #f0f4ff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-btn:disabled {
|
.page-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pageInfo {
|
#pageInfo {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(102, 126, 234, 0.1);
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 상세보기 페이지 스타일 */
|
||||||
|
.detail-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #5a67d8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info {
|
||||||
|
display: grid;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.description {
|
||||||
|
min-height: 80px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.no-files {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachments-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-single-btn {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-single-btn:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-actions {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-all-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-all-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모바일 반응형 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-section {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-single-btn {
|
||||||
|
align-self: stretch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list {
|
.file-list {
|
||||||
|
Reference in New Issue
Block a user