534 lines
18 KiB
JavaScript
534 lines
18 KiB
JavaScript
// ========================================
|
||
// Gallery 페이지 전용 JavaScript
|
||
// ========================================
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initGallery();
|
||
initLightbox();
|
||
initGalleryAnimations();
|
||
initAFrame360Viewers();
|
||
});
|
||
|
||
// 갤러리 초기화
|
||
function initGallery() {
|
||
const galleryItems = document.querySelectorAll('.gallery-item');
|
||
|
||
galleryItems.forEach((item, index) => {
|
||
const img = item.querySelector('.gallery-img');
|
||
|
||
// 이미지 클릭 시 라이트박스 열기
|
||
img.addEventListener('click', () => openLightbox(index));
|
||
|
||
// 이미지 로딩 에러 처리
|
||
img.addEventListener('error', function() {
|
||
this.src = 'images/placeholder.jpg';
|
||
this.alt = '이미지를 불러올 수 없습니다';
|
||
});
|
||
|
||
// 레이지 로딩 구현
|
||
if ('IntersectionObserver' in window) {
|
||
const imageObserver = new IntersectionObserver((entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
const img = entry.target;
|
||
img.src = img.dataset.src || img.src;
|
||
img.classList.remove('lazy');
|
||
imageObserver.unobserve(img);
|
||
}
|
||
});
|
||
});
|
||
|
||
if (img.dataset.src) {
|
||
imageObserver.observe(img);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 라이트박스 기능
|
||
let currentImageIndex = 0;
|
||
const galleryImages = document.querySelectorAll('.gallery-img');
|
||
|
||
function initLightbox() {
|
||
// 라이트박스 HTML 생성
|
||
const lightboxHTML = `
|
||
<div id="lightbox" class="lightbox">
|
||
<div class="lightbox-content">
|
||
<img id="lightbox-img" class="lightbox-img" src="" alt="">
|
||
<div id="lightbox-caption" class="lightbox-caption"></div>
|
||
<button class="lightbox-close" onclick="closeLightbox()"></button>
|
||
<button class="lightbox-nav lightbox-prev" onclick="previousImage()">‹</button>
|
||
<button class="lightbox-nav lightbox-next" onclick="nextImage()">›</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.insertAdjacentHTML('beforeend', lightboxHTML);
|
||
|
||
// ESC 키로 라이트박스 닫기
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') closeLightbox();
|
||
if (e.key === 'ArrowLeft') previousImage();
|
||
if (e.key === 'ArrowRight') nextImage();
|
||
});
|
||
|
||
// 배경 클릭으로 라이트박스 닫기
|
||
document.getElementById('lightbox').addEventListener('click', function(e) {
|
||
if (e.target === this) closeLightbox();
|
||
});
|
||
}
|
||
|
||
function openLightbox(index) {
|
||
currentImageIndex = index;
|
||
const lightbox = document.getElementById('lightbox');
|
||
const lightboxImg = document.getElementById('lightbox-img');
|
||
const lightboxCaption = document.getElementById('lightbox-caption');
|
||
|
||
const currentImg = galleryImages[index];
|
||
const caption = currentImg.closest('.gallery-item').querySelector('.gallery-caption');
|
||
|
||
lightboxImg.src = currentImg.src;
|
||
lightboxImg.alt = currentImg.alt;
|
||
lightboxCaption.textContent = caption ? caption.textContent : currentImg.alt;
|
||
|
||
lightbox.classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeLightbox() {
|
||
const lightbox = document.getElementById('lightbox');
|
||
lightbox.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
function nextImage() {
|
||
currentImageIndex = (currentImageIndex + 1) % galleryImages.length;
|
||
openLightbox(currentImageIndex);
|
||
}
|
||
|
||
function previousImage() {
|
||
currentImageIndex = (currentImageIndex - 1 + galleryImages.length) % galleryImages.length;
|
||
openLightbox(currentImageIndex);
|
||
}
|
||
|
||
// 갤러리 애니메이션
|
||
function initGalleryAnimations() {
|
||
const galleryItems = document.querySelectorAll('.gallery-item');
|
||
|
||
// Intersection Observer를 사용한 애니메이션
|
||
const observerOptions = {
|
||
threshold: 0.1,
|
||
rootMargin: '50px 0px'
|
||
};
|
||
|
||
const observer = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry, index) => {
|
||
if (entry.isIntersecting) {
|
||
setTimeout(() => {
|
||
entry.target.style.opacity = '1';
|
||
entry.target.style.transform = 'translateY(0)';
|
||
}, index * 100);
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, observerOptions);
|
||
|
||
galleryItems.forEach(item => {
|
||
item.style.opacity = '0';
|
||
item.style.transform = 'translateY(30px)';
|
||
item.style.transition = 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)';
|
||
observer.observe(item);
|
||
});
|
||
}
|
||
|
||
// 갤러리 필터 기능 (향후 확장용)
|
||
function initGalleryFilters() {
|
||
const filterButtons = document.querySelectorAll('.filter-btn');
|
||
const galleryItems = document.querySelectorAll('.gallery-item');
|
||
|
||
filterButtons.forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
// 활성 버튼 업데이트
|
||
filterButtons.forEach(b => b.classList.remove('active'));
|
||
this.classList.add('active');
|
||
|
||
const filter = this.dataset.filter;
|
||
|
||
galleryItems.forEach(item => {
|
||
if (filter === 'all' || item.dataset.category === filter) {
|
||
item.style.display = 'block';
|
||
setTimeout(() => {
|
||
item.style.opacity = '1';
|
||
item.style.transform = 'scale(1)';
|
||
}, 10);
|
||
} else {
|
||
item.style.opacity = '0';
|
||
item.style.transform = 'scale(0.8)';
|
||
setTimeout(() => {
|
||
item.style.display = 'none';
|
||
}, 300);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// 이미지 프리로딩
|
||
function preloadImages() {
|
||
galleryImages.forEach(img => {
|
||
const imagePreload = new Image();
|
||
imagePreload.src = img.src;
|
||
});
|
||
}
|
||
|
||
// 갤러리 검색 기능 (향후 확장용)
|
||
function initGallerySearch() {
|
||
const searchInput = document.getElementById('gallery-search');
|
||
const galleryItems = document.querySelectorAll('.gallery-item');
|
||
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', function() {
|
||
const searchTerm = this.value.toLowerCase();
|
||
|
||
galleryItems.forEach(item => {
|
||
const caption = item.querySelector('.gallery-caption');
|
||
const alt = item.querySelector('.gallery-img').alt;
|
||
const text = (caption ? caption.textContent : '') + ' ' + alt;
|
||
|
||
if (text.toLowerCase().includes(searchTerm)) {
|
||
item.style.display = 'block';
|
||
} else {
|
||
item.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// 이미지 로딩 상태 표시
|
||
function showGalleryLoading() {
|
||
const loading = document.querySelector('.gallery-loading');
|
||
if (loading) {
|
||
loading.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function hideGalleryLoading() {
|
||
const loading = document.querySelector('.gallery-loading');
|
||
if (loading) {
|
||
loading.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 갤러리 그리드 리사이즈 최적화
|
||
let resizeTimeout;
|
||
window.addEventListener('resize', function() {
|
||
clearTimeout(resizeTimeout);
|
||
resizeTimeout = setTimeout(function() {
|
||
// 갤러리 그리드 재조정 로직
|
||
const galleryGrid = document.querySelector('.gallery-grid');
|
||
if (galleryGrid) {
|
||
galleryGrid.style.opacity = '0.8';
|
||
setTimeout(() => {
|
||
galleryGrid.style.opacity = '1';
|
||
}, 100);
|
||
}
|
||
}, 250);
|
||
});
|
||
|
||
// 터치 이벤트 지원 (모바일)
|
||
let touchStartX = 0;
|
||
let touchEndX = 0;
|
||
|
||
document.addEventListener('touchstart', function(e) {
|
||
if (document.getElementById('lightbox').classList.contains('active')) {
|
||
touchStartX = e.changedTouches[0].screenX;
|
||
}
|
||
});
|
||
|
||
document.addEventListener('touchend', function(e) {
|
||
if (document.getElementById('lightbox').classList.contains('active')) {
|
||
touchEndX = e.changedTouches[0].screenX;
|
||
handleSwipe();
|
||
}
|
||
});
|
||
|
||
function handleSwipe() {
|
||
const swipeThreshold = 50;
|
||
const diff = touchStartX - touchEndX;
|
||
|
||
if (Math.abs(diff) > swipeThreshold) {
|
||
if (diff > 0) {
|
||
nextImage(); // 왼쪽으로 스와이프 = 다음 이미지
|
||
} else {
|
||
previousImage(); // 오른쪽으로 스와이프 = 이전 이미지
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// A-Frame 기반 360도 VR 뷰어 - 안정적이고 강력한 VR 체험
|
||
// ========================================
|
||
|
||
let currentAFrameScene = null;
|
||
let autoRotationInterval = null;
|
||
let isAutoRotating = false;
|
||
|
||
function initAFrame360Viewers() {
|
||
// 모달 생성
|
||
createAFrameModal();
|
||
|
||
// 미리보기 설정
|
||
initAFramePreviews();
|
||
}
|
||
|
||
function initAFramePreviews() {
|
||
const containers = document.querySelectorAll('.panorama-clickable');
|
||
|
||
containers.forEach(container => {
|
||
const imageSrc = container.dataset.image;
|
||
const title = container.dataset.title;
|
||
|
||
// 클릭 이벤트
|
||
container.addEventListener('click', () => {
|
||
openAFrameModal(imageSrc, title);
|
||
});
|
||
|
||
// 도움말 자동 숨김
|
||
setTimeout(() => {
|
||
const help = container.querySelector('.panorama-help');
|
||
if (help) {
|
||
help.style.opacity = '0';
|
||
setTimeout(() => help.remove(), 1000);
|
||
}
|
||
}, 4000);
|
||
});
|
||
}
|
||
|
||
function createAFrameModal() {
|
||
const modalHTML = `
|
||
<div id="aframe-modal" class="panorama-modal">
|
||
<div class="panorama-modal-content">
|
||
<div id="aframe-viewer" class="panorama-modal-viewer">
|
||
<!-- A-Frame Scene will be injected here -->
|
||
</div>
|
||
<div class="panorama-modal-controls">
|
||
<div id="aframe-modal-title" class="panorama-modal-title"></div>
|
||
<div class="panorama-modal-buttons">
|
||
<button class="panorama-modal-btn" id="aframe-reset-btn">
|
||
<i class="fas fa-home"></i> 리셋
|
||
</button>
|
||
<button class="panorama-modal-btn" id="aframe-auto-btn">
|
||
<i class="fas fa-sync-alt"></i> 자동회전
|
||
</button>
|
||
<button class="panorama-modal-btn" id="aframe-vr-btn">
|
||
<i class="fas fa-vr-cardboard"></i> VR모드
|
||
</button>
|
||
<button class="panorama-modal-btn" id="aframe-help-btn">
|
||
<i class="fas fa-question-circle"></i> 도움말
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button class="panorama-modal-close" id="aframe-modal-close">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
<div class="panorama-modal-help-text" id="aframe-help-text">
|
||
<p><strong>360° VR 조작법</strong></p>
|
||
<p>🖱️ 드래그: 전방향 회전</p>
|
||
<p>📱 터치: 터치 후 드래그</p>
|
||
<p>🖱️ 휠: 줌 인/아웃</p>
|
||
<p>📱 핀치: 줌 인/아웃</p>
|
||
<p>⌨️ WASD: 이동</p>
|
||
<p>📱 VR모드: VR 헤드셋 지원</p>
|
||
<p>⌨️ ESC: 닫기</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||
setupAFrameModalListeners();
|
||
}
|
||
|
||
function setupAFrameModalListeners() {
|
||
const modal = document.getElementById('aframe-modal');
|
||
const closeBtn = document.getElementById('aframe-modal-close');
|
||
const resetBtn = document.getElementById('aframe-reset-btn');
|
||
const autoBtn = document.getElementById('aframe-auto-btn');
|
||
const vrBtn = document.getElementById('aframe-vr-btn');
|
||
const helpBtn = document.getElementById('aframe-help-btn');
|
||
const helpText = document.getElementById('aframe-help-text');
|
||
|
||
let helpVisible = false;
|
||
|
||
// 모달 닫기
|
||
closeBtn.addEventListener('click', closeAFrameModal);
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) closeAFrameModal();
|
||
});
|
||
|
||
// ESC 키
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||
closeAFrameModal();
|
||
}
|
||
});
|
||
|
||
// 리셋 버튼
|
||
resetBtn.addEventListener('click', () => {
|
||
if (currentAFrameScene) {
|
||
const camera = currentAFrameScene.querySelector('[camera]');
|
||
if (camera) {
|
||
camera.setAttribute('rotation', '0 0 0');
|
||
camera.setAttribute('position', '0 0 0');
|
||
}
|
||
}
|
||
});
|
||
|
||
// 자동 회전 버튼
|
||
autoBtn.addEventListener('click', () => {
|
||
if (isAutoRotating) {
|
||
stopAutoRotation();
|
||
autoBtn.innerHTML = '<i class="fas fa-sync-alt"></i> 자동회전';
|
||
autoBtn.classList.remove('active');
|
||
} else {
|
||
startAutoRotation();
|
||
autoBtn.innerHTML = '<i class="fas fa-pause"></i> 정지';
|
||
autoBtn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// VR 버튼
|
||
vrBtn.addEventListener('click', () => {
|
||
if (currentAFrameScene) {
|
||
const vrButton = currentAFrameScene.querySelector('[vr-mode-ui]');
|
||
if (vrButton) {
|
||
// VR 모드 진입
|
||
currentAFrameScene.enterVR();
|
||
}
|
||
}
|
||
});
|
||
|
||
// 도움말 버튼
|
||
helpBtn.addEventListener('click', () => {
|
||
helpVisible = !helpVisible;
|
||
helpText.style.display = helpVisible ? 'block' : 'none';
|
||
helpBtn.classList.toggle('active', helpVisible);
|
||
});
|
||
|
||
// 도움말 자동 숨김
|
||
setTimeout(() => {
|
||
helpText.style.display = 'none';
|
||
}, 6000);
|
||
}
|
||
|
||
function openAFrameModal(imageSrc, title) {
|
||
const modal = document.getElementById('aframe-modal');
|
||
const modalTitle = document.getElementById('aframe-modal-title');
|
||
const viewerContainer = document.getElementById('aframe-viewer');
|
||
|
||
modalTitle.textContent = title;
|
||
modal.classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// A-Frame Scene 생성
|
||
const sceneHTML = `
|
||
<a-scene
|
||
id="aframe-scene"
|
||
embedded
|
||
style="height: 100%; width: 100%;"
|
||
vr-mode-ui="enabled: true"
|
||
background="color: #000000">
|
||
|
||
<!-- 360도 이미지 스카이박스 -->
|
||
<a-sky
|
||
id="panorama-sky"
|
||
src="${imageSrc}"
|
||
rotation="0 -90 0">
|
||
</a-sky>
|
||
|
||
<!-- 카메라 (사용자 시점) -->
|
||
<a-camera
|
||
id="panorama-camera"
|
||
look-controls="enabled: true; touchEnabled: true"
|
||
wasd-controls="enabled: false"
|
||
position="0 0 0"
|
||
rotation="0 0 0"
|
||
fov="80"
|
||
near="0.1"
|
||
far="1000">
|
||
|
||
<!-- 커서 (VR 모드용) -->
|
||
<a-cursor
|
||
animation__click="property: scale; startEvents: click; from: 0.1 0.1 0.1; to: 1 1 1; dur: 150"
|
||
animation__fusing="property: scale; startEvents: fusing; from: 1 1 1; to: 0.1 0.1 0.1; dur: 1500"
|
||
raycaster="objects: .clickable"
|
||
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03"
|
||
material="color: white; shader: flat">
|
||
</a-cursor>
|
||
</a-camera>
|
||
|
||
<!-- 조명 -->
|
||
<a-light type="ambient" color="#404040" intensity="0.4"></a-light>
|
||
<a-light type="directional" position="1 1 1" color="#ffffff" intensity="0.6"></a-light>
|
||
</a-scene>
|
||
`;
|
||
|
||
viewerContainer.innerHTML = sceneHTML;
|
||
currentAFrameScene = document.getElementById('aframe-scene');
|
||
|
||
// A-Frame 로딩 완료 대기
|
||
currentAFrameScene.addEventListener('loaded', () => {
|
||
console.log('A-Frame 360도 뷰어 로드 완료:', title);
|
||
});
|
||
}
|
||
|
||
function startAutoRotation() {
|
||
if (!currentAFrameScene) return;
|
||
|
||
isAutoRotating = true;
|
||
const camera = currentAFrameScene.querySelector('#panorama-camera');
|
||
let currentRotationY = 0;
|
||
|
||
autoRotationInterval = setInterval(() => {
|
||
if (camera && isAutoRotating) {
|
||
currentRotationY += 0.5; // 초당 0.5도 회전
|
||
if (currentRotationY >= 360) currentRotationY = 0;
|
||
|
||
const currentRotation = camera.getAttribute('rotation');
|
||
camera.setAttribute('rotation', `${currentRotation.x} ${currentRotationY} ${currentRotation.z}`);
|
||
}
|
||
}, 50); // 20fps
|
||
}
|
||
|
||
function stopAutoRotation() {
|
||
isAutoRotating = false;
|
||
if (autoRotationInterval) {
|
||
clearInterval(autoRotationInterval);
|
||
autoRotationInterval = null;
|
||
}
|
||
}
|
||
|
||
function closeAFrameModal() {
|
||
const modal = document.getElementById('aframe-modal');
|
||
const autoBtn = document.getElementById('aframe-auto-btn');
|
||
|
||
// 자동 회전 정지
|
||
stopAutoRotation();
|
||
|
||
// 버튼 초기화
|
||
autoBtn.innerHTML = '<i class="fas fa-sync-alt"></i> 자동회전';
|
||
autoBtn.classList.remove('active');
|
||
|
||
// A-Frame Scene 정리
|
||
if (currentAFrameScene) {
|
||
currentAFrameScene.destroy();
|
||
currentAFrameScene = null;
|
||
}
|
||
|
||
modal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
|
||
// 컨테이너 정리
|
||
document.getElementById('aframe-viewer').innerHTML = '';
|
||
} |