mingle-website/js/gallery.js

534 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ========================================
// 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 = '';
}