350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
/**
|
|
* 배경 씬 라이브러리 JavaScript
|
|
* Unity에서 업로드된 JSON 데이터를 기반으로 배경 목록을 표시
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// 데이터 파일 경로
|
|
const DATA_PATH = 'data/backgrounds.json';
|
|
|
|
// 상태
|
|
let backgroundsData = [];
|
|
let filteredData = [];
|
|
let currentTag = 'all';
|
|
let currentView = 'grid';
|
|
let searchQuery = '';
|
|
|
|
// DOM 요소
|
|
const elements = {
|
|
grid: document.getElementById('backgroundsGrid'),
|
|
filterTags: document.getElementById('filterTags'),
|
|
searchInput: document.getElementById('searchInput'),
|
|
totalCount: document.getElementById('totalCount'),
|
|
filteredCount: document.getElementById('filteredCount'),
|
|
lastUpdated: document.getElementById('lastUpdated'),
|
|
noData: document.getElementById('noData'),
|
|
noResults: document.getElementById('noResults'),
|
|
imageModal: document.getElementById('imageModal'),
|
|
modalImage: document.getElementById('modalImage'),
|
|
modalTitle: document.getElementById('modalTitle'),
|
|
modalCategory: document.getElementById('modalCategory'),
|
|
modalTags: document.getElementById('modalTags')
|
|
};
|
|
|
|
/**
|
|
* 초기화
|
|
*/
|
|
async function init() {
|
|
await loadData();
|
|
setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* 데이터 로드
|
|
*/
|
|
async function loadData() {
|
|
try {
|
|
const response = await fetch(DATA_PATH + '?t=' + Date.now());
|
|
|
|
if (!response.ok) {
|
|
throw new Error('데이터를 찾을 수 없습니다');
|
|
}
|
|
|
|
const data = await response.json();
|
|
backgroundsData = data.backgrounds || [];
|
|
|
|
// 마지막 업데이트 시간 표시
|
|
if (data.lastUpdated) {
|
|
const date = new Date(data.lastUpdated);
|
|
elements.lastUpdated.textContent = `마지막 업데이트: ${formatDate(date)}`;
|
|
}
|
|
|
|
// 태그 필터 생성
|
|
createTagFilters();
|
|
|
|
// 통계 업데이트
|
|
elements.totalCount.textContent = backgroundsData.length;
|
|
|
|
// 데이터 렌더링
|
|
filterAndRender();
|
|
|
|
} catch (error) {
|
|
console.error('데이터 로드 실패:', error);
|
|
showNoData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 태그 필터 버튼 생성
|
|
*/
|
|
function createTagFilters() {
|
|
// 모든 태그 수집
|
|
const allTags = new Set();
|
|
backgroundsData.forEach(bg => {
|
|
if (bg.tags && Array.isArray(bg.tags)) {
|
|
bg.tags.forEach(tag => allTags.add(tag));
|
|
}
|
|
});
|
|
|
|
// 기존 태그 버튼 제거 (전체 버튼 제외)
|
|
const existingTags = elements.filterTags.querySelectorAll('.filter-tag:not([data-tag="all"])');
|
|
existingTags.forEach(tag => tag.remove());
|
|
|
|
// 태그 버튼 생성
|
|
allTags.forEach(tag => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'filter-tag';
|
|
btn.dataset.tag = tag;
|
|
btn.textContent = tag;
|
|
btn.addEventListener('click', () => setTagFilter(tag));
|
|
elements.filterTags.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 이벤트 리스너 설정
|
|
*/
|
|
function setupEventListeners() {
|
|
// 검색
|
|
elements.searchInput.addEventListener('input', debounce((e) => {
|
|
searchQuery = e.target.value.toLowerCase();
|
|
filterAndRender();
|
|
}, 300));
|
|
|
|
// 전체 태그 필터
|
|
const allTagBtn = elements.filterTags.querySelector('[data-tag="all"]');
|
|
if (allTagBtn) {
|
|
allTagBtn.addEventListener('click', () => setTagFilter('all'));
|
|
}
|
|
|
|
// 뷰 전환
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentView = btn.dataset.view;
|
|
updateViewMode();
|
|
});
|
|
});
|
|
|
|
// 모달 닫기
|
|
elements.imageModal.querySelector('.modal-overlay').addEventListener('click', closeModal);
|
|
elements.imageModal.querySelector('.modal-close').addEventListener('click', closeModal);
|
|
|
|
// ESC 키로 모달 닫기
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeModal();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 태그 필터 설정
|
|
*/
|
|
function setTagFilter(tag) {
|
|
currentTag = tag;
|
|
|
|
// 버튼 활성화 상태 업데이트
|
|
document.querySelectorAll('.filter-tag').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.tag === tag);
|
|
});
|
|
|
|
filterAndRender();
|
|
}
|
|
|
|
/**
|
|
* 필터링 및 렌더링
|
|
*/
|
|
function filterAndRender() {
|
|
// 필터링
|
|
filteredData = backgroundsData.filter(bg => {
|
|
// 태그 필터
|
|
if (currentTag !== 'all') {
|
|
if (!bg.tags || !bg.tags.includes(currentTag)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 검색 필터
|
|
if (searchQuery) {
|
|
const searchTarget = `${bg.sceneName} ${bg.categoryName} ${(bg.tags || []).join(' ')}`.toLowerCase();
|
|
if (!searchTarget.includes(searchQuery)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// 통계 업데이트
|
|
elements.filteredCount.textContent = filteredData.length;
|
|
|
|
// 렌더링
|
|
render();
|
|
}
|
|
|
|
/**
|
|
* 카드 렌더링
|
|
*/
|
|
function render() {
|
|
// 로딩/에러 상태 숨기기
|
|
const loadingPlaceholder = elements.grid.querySelector('.loading-placeholder');
|
|
if (loadingPlaceholder) {
|
|
loadingPlaceholder.remove();
|
|
}
|
|
|
|
elements.noData.style.display = 'none';
|
|
elements.noResults.style.display = 'none';
|
|
|
|
// 데이터 없음
|
|
if (backgroundsData.length === 0) {
|
|
elements.grid.innerHTML = '';
|
|
showNoData();
|
|
return;
|
|
}
|
|
|
|
// 검색 결과 없음
|
|
if (filteredData.length === 0) {
|
|
elements.grid.innerHTML = '';
|
|
elements.noResults.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// 카드 생성
|
|
elements.grid.innerHTML = filteredData.map(bg => createCard(bg)).join('');
|
|
|
|
// 카드 클릭 이벤트
|
|
elements.grid.querySelectorAll('.background-card').forEach((card, index) => {
|
|
card.addEventListener('click', () => openModal(filteredData[index]));
|
|
});
|
|
|
|
// 뷰 모드 적용
|
|
updateViewMode();
|
|
}
|
|
|
|
/**
|
|
* 카드 HTML 생성
|
|
*/
|
|
function createCard(bg) {
|
|
const thumbnailHtml = bg.thumbnailUrl
|
|
? `<img src="${escapeHtml(bg.thumbnailUrl)}" alt="${escapeHtml(bg.sceneName)}" loading="lazy">`
|
|
: `<div class="no-thumbnail">🏞️</div>`;
|
|
|
|
const tagsHtml = (bg.tags || [])
|
|
.map(tag => `<span class="card-tag">${escapeHtml(tag)}</span>`)
|
|
.join('');
|
|
|
|
const category = bg.category || extractCategory(bg.categoryName);
|
|
|
|
return `
|
|
<div class="background-card" data-scene="${escapeHtml(bg.sceneName)}">
|
|
<div class="card-thumbnail">
|
|
${thumbnailHtml}
|
|
<span class="card-category">${escapeHtml(category)}</span>
|
|
</div>
|
|
<div class="card-content">
|
|
<h3 class="card-title">${escapeHtml(bg.sceneName)}</h3>
|
|
<div class="card-tags">${tagsHtml}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 뷰 모드 업데이트
|
|
*/
|
|
function updateViewMode() {
|
|
elements.grid.classList.toggle('list-view', currentView === 'list');
|
|
}
|
|
|
|
/**
|
|
* 모달 열기
|
|
*/
|
|
function openModal(bg) {
|
|
if (bg.thumbnailUrl) {
|
|
elements.modalImage.src = bg.thumbnailUrl;
|
|
elements.modalImage.alt = bg.sceneName;
|
|
} else {
|
|
elements.modalImage.src = '';
|
|
elements.modalImage.alt = '';
|
|
}
|
|
|
|
elements.modalTitle.textContent = bg.sceneName;
|
|
elements.modalCategory.textContent = bg.categoryName;
|
|
|
|
elements.modalTags.innerHTML = (bg.tags || [])
|
|
.map(tag => `<span class="card-tag">${escapeHtml(tag)}</span>`)
|
|
.join('');
|
|
|
|
elements.imageModal.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
/**
|
|
* 모달 닫기
|
|
*/
|
|
function closeModal() {
|
|
elements.imageModal.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
/**
|
|
* 데이터 없음 표시
|
|
*/
|
|
function showNoData() {
|
|
elements.grid.innerHTML = '';
|
|
elements.noData.style.display = 'block';
|
|
}
|
|
|
|
/**
|
|
* 카테고리 추출 (폴더명에서)
|
|
*/
|
|
function extractCategory(folderName) {
|
|
if (!folderName) return '기타';
|
|
const match = folderName.match(/\[([^\]]+)\]/);
|
|
return match ? match[1] : folderName;
|
|
}
|
|
|
|
/**
|
|
* 날짜 포맷
|
|
*/
|
|
function formatDate(date) {
|
|
return date.toLocaleString('ko-KR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* HTML 이스케이프
|
|
*/
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* 디바운스
|
|
*/
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
})();
|