// ======================================== // Gallery 페이지 전용 JavaScript // ======================================== document.addEventListener('DOMContentLoaded', function() { initGallery(); initLightbox(); initGalleryAnimations(); initCustom360Viewers(); }); // 갤러리 초기화 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 = ` `; 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(); // 오른쪽으로 스와이프 = 이전 이미지 } } } // ======================================== // 간단한 360도 파노라마 뷰어 - 좌우 스크롤 방식 // ======================================== class Easy360Viewer { constructor(container, imageSrc, title) { this.container = container; this.imageSrc = imageSrc; this.title = title; // 뷰어 상태 this.currentX = 0; // 현재 X 위치 this.maxX = 0; // 최대 스크롤 가능 X this.zoom = 1; // 마우스/터치 상태 this.isDragging = false; this.startX = 0; this.lastX = 0; this.velocity = 0; // 자동 회전 this.isAutoRotating = false; this.autoRotateSpeed = 1; this.animationId = null; // 터치 줌 this.touchCount = 0; this.lastPinchDistance = 0; this.init(); } init() { this.createViewer(); this.bindEvents(); this.startAnimation(); } createViewer() { // 컨테이너 스타일 this.container.style.cssText = ` position: relative; width: 100%; height: 100%; overflow: hidden; background: #000; cursor: grab; user-select: none; `; // 360도 이미지 래퍼 this.imageWrapper = document.createElement('div'); this.imageWrapper.style.cssText = ` position: absolute; top: 0; left: 0; height: 100%; will-change: transform; display: flex; align-items: center; `; // 로딩 표시 this.showLoading(); // 메인 360도 이미지 this.image = document.createElement('img'); this.image.src = this.imageSrc; this.image.style.cssText = ` height: 100%; width: auto; object-fit: cover; user-select: none; pointer-events: none; display: block; `; this.image.draggable = false; // 이미지 로드 완료 후 this.image.onload = () => { this.hideLoading(); this.setupImageSize(); this.createDuplicateImages(); this.updateTransform(); }; this.image.onerror = () => { this.hideLoading(); this.showError(); }; this.imageWrapper.appendChild(this.image); this.container.appendChild(this.imageWrapper); } setupImageSize() { // 이미지 자연 어숙비를 유지하면서 컨테이너 높이에 맞춤 const containerHeight = this.container.clientHeight; const imageAspectRatio = this.image.naturalWidth / this.image.naturalHeight; const imageWidth = containerHeight * imageAspectRatio; // 360도 이미지는 보통 매우 가로가 김 this.imageWrapper.style.width = imageWidth + 'px'; // 스크롤 가능 범위 계산 this.maxX = Math.max(0, imageWidth - this.container.clientWidth); } createDuplicateImages() { // 무한 스크롤을 위해 이미지 복사본 생성 const imageClone1 = this.image.cloneNode(); const imageClone2 = this.image.cloneNode(); imageClone1.style.cssText = this.image.style.cssText; imageClone2.style.cssText = this.image.style.cssText; this.imageWrapper.appendChild(imageClone1); this.imageWrapper.appendChild(imageClone2); // 래퍼 전체 너비를 3배로 const currentWidth = parseInt(this.imageWrapper.style.width); this.imageWrapper.style.width = (currentWidth * 3) + 'px'; // 스크롤 범위 업데이트 this.maxX = currentWidth * 2; // 시작 위치를 중앙 이미지로 this.currentX = currentWidth; this.updateTransform(); } showLoading() { const loader = document.createElement('div'); loader.className = 'panorama-loader'; loader.innerHTML = `

360° 이미지 로딩중...

`; loader.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #fff; z-index: 10; pointer-events: none; `; this.container.appendChild(loader); this.loadingElement = loader; } hideLoading() { if (this.loadingElement) { this.loadingElement.remove(); this.loadingElement = null; } } showError() { const errorElement = document.createElement('div'); errorElement.innerHTML = `
😞
이미지를 불러올 수 없습니다
이미지 파일을 확인해주세요
`; errorElement.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; `; this.container.appendChild(errorElement); } bindEvents() { // 마우스 이벤트 this.container.addEventListener('mousedown', this.handleStart.bind(this)); this.container.addEventListener('mousemove', this.handleMove.bind(this)); this.container.addEventListener('mouseup', this.handleEnd.bind(this)); this.container.addEventListener('mouseleave', this.handleEnd.bind(this)); // 터치 이벤트 this.container.addEventListener('touchstart', this.handleStart.bind(this), { passive: false }); this.container.addEventListener('touchmove', this.handleMove.bind(this), { passive: false }); this.container.addEventListener('touchend', this.handleEnd.bind(this)); // 휠 이벤트 (좌우 스크롤) this.container.addEventListener('wheel', this.handleWheel.bind(this), { passive: false }); // 컨텍스트 메뉴 방지 this.container.addEventListener('contextmenu', e => e.preventDefault()); // 리사이즈 window.addEventListener('resize', this.handleResize.bind(this)); } handleStart(e) { e.preventDefault(); this.isDragging = true; this.velocity = 0; this.container.style.cursor = 'grabbing'; this.stopAutoRotate(); if (e.touches) { this.touchCount = e.touches.length; if (this.touchCount === 1) { this.startX = e.touches[0].clientX; } else if (this.touchCount === 2) { // 핀치 줌 초기화 const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; this.lastPinchDistance = Math.sqrt(dx * dx + dy * dy); return; // 핀치일 때는 드래그 비활성화 } } else { this.startX = e.clientX; } this.lastX = this.startX; } handleMove(e) { if (!this.isDragging) return; e.preventDefault(); let currentX; if (e.touches) { if (this.touchCount === 1 && e.touches.length === 1) { currentX = e.touches[0].clientX; this.updatePosition(currentX); } else if (this.touchCount === 2 && e.touches.length === 2) { // 핀치 줌 처리 const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const distance = Math.sqrt(dx * dx + dy * dy); if (this.lastPinchDistance > 0) { const scale = distance / this.lastPinchDistance; this.zoom = Math.max(0.8, Math.min(2.5, this.zoom * scale)); this.updateTransform(); } this.lastPinchDistance = distance; return; } } else { currentX = e.clientX; this.updatePosition(currentX); } } updatePosition(currentX) { const deltaX = currentX - this.lastX; // 좌우 이동 (드래그 방향과 반대) this.currentX -= deltaX; // 속도 계산 (관성용) this.velocity = -deltaX * 0.1; this.updateTransform(); this.lastX = currentX; } handleEnd() { this.isDragging = false; this.touchCount = 0; this.lastPinchDistance = 0; this.container.style.cursor = 'grab'; } handleWheel(e) { e.preventDefault(); // 휠로 좌우 스크롤 const scrollSpeed = 50; this.currentX += e.deltaY > 0 ? scrollSpeed : -scrollSpeed; this.updateTransform(); } handleResize() { if (this.image && this.image.complete) { this.setupImageSize(); this.updateTransform(); } } updateTransform() { if (!this.imageWrapper) return; // 무한 스크롤 처리 const singleImageWidth = parseInt(this.imageWrapper.style.width) / 3; if (this.currentX < 0) { this.currentX += singleImageWidth; } else if (this.currentX > singleImageWidth * 2) { this.currentX -= singleImageWidth; } const transform = `translateX(${-this.currentX}px) scale(${this.zoom})`; this.imageWrapper.style.transform = transform; } startAnimation() { const animate = () => { // 관성 적용 if (!this.isDragging && Math.abs(this.velocity) > 0.5) { this.currentX += this.velocity; this.velocity *= 0.95; // 마찰력 this.updateTransform(); } // 자동 회전 if (this.isAutoRotating && !this.isDragging) { this.currentX += this.autoRotateSpeed; this.updateTransform(); } this.animationId = requestAnimationFrame(animate); }; animate(); } reset() { this.currentX = parseInt(this.imageWrapper.style.width) / 3; // 중앙 이미지로 this.zoom = 1; this.velocity = 0; this.stopAutoRotate(); this.updateTransform(); } startAutoRotate() { this.isAutoRotating = true; } stopAutoRotate() { this.isAutoRotating = false; } zoomIn() { this.zoom = Math.min(2.5, this.zoom * 1.2); this.updateTransform(); } zoomOut() { this.zoom = Math.max(0.8, this.zoom * 0.8); this.updateTransform(); } destroy() { this.stopAutoRotate(); if (this.animationId) { cancelAnimationFrame(this.animationId); } window.removeEventListener('resize', this.handleResize.bind(this)); if (this.container) { this.container.innerHTML = ''; } } } let current360Viewer = null; function initCustom360Viewers() { createPanoramaModal(); initPanoramaPreviews(); } function initPanoramaPreviews() { const clickableElements = document.querySelectorAll('.panorama-clickable'); clickableElements.forEach(element => { const imageSrc = element.dataset.image; const title = element.dataset.title; element.addEventListener('click', () => { openPanoramaModal(imageSrc, title); }); // 호버 효과 element.addEventListener('mouseenter', () => { element.style.transform = 'scale(1.02)'; }); element.addEventListener('mouseleave', () => { element.style.transform = 'scale(1)'; }); // 도움말 자동 사라짐 setTimeout(() => { const helpText = element.querySelector('.panorama-help'); if (helpText) { helpText.style.opacity = '0'; setTimeout(() => helpText.remove(), 500); } }, 4000); }); } function createPanoramaModal() { const modalHTML = `

360° 조작 가이드

마우스 드래그로 화면을 회전시킬 수 있습니다
터치 스크린에서는 손가락으로 드래그하세요
마우스 휠이나 핀치로 확대/축소할 수 있습니다
자동회전 버튼으로 자동으로 둘러볼 수 있습니다
ESC 키를 눌러 닫을 수 있습니다
`; document.body.insertAdjacentHTML('beforeend', modalHTML); setupPanoramaModalEvents(); } function setupPanoramaModalEvents() { const modal = document.getElementById('panorama-modal'); const closeBtn = document.getElementById('panorama-close-btn'); const resetBtn = document.getElementById('panorama-reset-btn'); const autoBtn = document.getElementById('panorama-auto-btn'); const zoomInBtn = document.getElementById('panorama-zoom-in-btn'); const zoomOutBtn = document.getElementById('panorama-zoom-out-btn'); const helpBtn = document.getElementById('panorama-help-btn'); const helpPanel = document.getElementById('panorama-help-panel'); let isAutoRotating = false; let isHelpVisible = false; // 모달 닫기 closeBtn.addEventListener('click', closePanoramaModal); modal.addEventListener('click', (e) => { if (e.target === modal) closePanoramaModal(); }); // ESC 키로 닫기 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.classList.contains('active')) { closePanoramaModal(); } }); // 초기화 버튼 resetBtn.addEventListener('click', () => { if (current360Viewer) { current360Viewer.reset(); } }); // 자동회전 버튼 autoBtn.addEventListener('click', () => { if (current360Viewer) { if (isAutoRotating) { current360Viewer.stopAutoRotate(); autoBtn.innerHTML = '자동회전'; autoBtn.classList.remove('active'); isAutoRotating = false; } else { current360Viewer.startAutoRotate(); autoBtn.innerHTML = '정지'; autoBtn.classList.add('active'); isAutoRotating = true; } } }); // 확대 버튼 zoomInBtn.addEventListener('click', () => { if (current360Viewer) { current360Viewer.zoomIn(); } }); // 축소 버튼 zoomOutBtn.addEventListener('click', () => { if (current360Viewer) { current360Viewer.zoomOut(); } }); // 도움말 버튼 helpBtn.addEventListener('click', () => { isHelpVisible = !isHelpVisible; helpPanel.style.display = isHelpVisible ? 'block' : 'none'; helpBtn.classList.toggle('active', isHelpVisible); }); // 도움말 패널 클릭시 닫기 helpPanel.addEventListener('click', (e) => { if (e.target === helpPanel) { isHelpVisible = false; helpPanel.style.display = 'none'; helpBtn.classList.remove('active'); } }); } function openPanoramaModal(imageSrc, title) { const modal = document.getElementById('panorama-modal'); const modalTitle = document.getElementById('panorama-modal-title'); const viewerContainer = document.getElementById('panorama-viewer-container'); modalTitle.textContent = title; modal.classList.add('active'); document.body.style.overflow = 'hidden'; // 간단한 360도 좌우 스크롤 뷰어 생성 current360Viewer = new Easy360Viewer(viewerContainer, imageSrc, title); } function closePanoramaModal() { const modal = document.getElementById('panorama-modal'); const autoBtn = document.getElementById('panorama-auto-btn'); const helpBtn = document.getElementById('panorama-help-btn'); const helpPanel = document.getElementById('panorama-help-panel'); // WebGL 뷰어 정리 if (current360Viewer) { current360Viewer.destroy(); current360Viewer = null; } // UI 초기화 autoBtn.innerHTML = '자동회전'; autoBtn.classList.remove('active'); helpBtn.classList.remove('active'); helpPanel.style.display = 'none'; modal.classList.remove('active'); document.body.style.overflow = ''; // 컨테이너 정리 document.getElementById('panorama-viewer-container').innerHTML = ''; }