mingle-website/js/gallery.js

871 lines
29 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();
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 = `
<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(); // 오른쪽으로 스와이프 = 이전 이미지
}
}
}
// ========================================
// 간단한 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 = `
<div class="spinner"></div>
<p>360° 이미지 로딩중...</p>
`;
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 = `
<div style="text-align: center; color: #ff6b6b; padding: 20px;">
<div style="font-size: 48px; margin-bottom: 10px;">😞</div>
<div style="font-size: 18px; margin-bottom: 10px;">이미지를 불러올 수 없습니다</div>
<div style="font-size: 14px; opacity: 0.8;">이미지 파일을 확인해주세요</div>
</div>
`;
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 = `
<div id="panorama-modal" class="panorama-modal">
<div class="panorama-modal-content">
<div id="panorama-viewer-container" class="panorama-viewer-container">
<!-- 360도 뷰어가 여기에 들어갑니다 -->
</div>
<div class="panorama-modal-controls">
<div id="panorama-modal-title" class="panorama-modal-title"></div>
<div class="panorama-control-buttons">
<button class="panorama-btn" id="panorama-reset-btn">
<i class="fas fa-home"></i>
<span>초기화</span>
</button>
<button class="panorama-btn" id="panorama-auto-btn">
<i class="fas fa-sync-alt"></i>
<span>자동회전</span>
</button>
<button class="panorama-btn" id="panorama-zoom-in-btn">
<i class="fas fa-search-plus"></i>
<span>확대</span>
</button>
<button class="panorama-btn" id="panorama-zoom-out-btn">
<i class="fas fa-search-minus"></i>
<span>축소</span>
</button>
<button class="panorama-btn" id="panorama-help-btn">
<i class="fas fa-question-circle"></i>
<span>도움말</span>
</button>
</div>
</div>
<button class="panorama-modal-close" id="panorama-close-btn">
<i class="fas fa-times"></i>
</button>
<div class="panorama-help-panel" id="panorama-help-panel">
<h3>360° 조작 가이드</h3>
<div class="help-content">
<div class="help-item">
<i class="fas fa-mouse"></i>
<span>마우스 드래그로 화면을 회전시킬 수 있습니다</span>
</div>
<div class="help-item">
<i class="fas fa-hand-pointer"></i>
<span>터치 스크린에서는 손가락으로 드래그하세요</span>
</div>
<div class="help-item">
<i class="fas fa-search"></i>
<span>마우스 휠이나 핀치로 확대/축소할 수 있습니다</span>
</div>
<div class="help-item">
<i class="fas fa-sync-alt"></i>
<span>자동회전 버튼으로 자동으로 둘러볼 수 있습니다</span>
</div>
<div class="help-item">
<i class="fas fa-keyboard"></i>
<span>ESC 키를 눌러 닫을 수 있습니다</span>
</div>
</div>
</div>
</div>
</div>
`;
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 = '<i class="fas fa-sync-alt"></i><span>자동회전</span>';
autoBtn.classList.remove('active');
isAutoRotating = false;
} else {
current360Viewer.startAutoRotate();
autoBtn.innerHTML = '<i class="fas fa-pause"></i><span>정지</span>';
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 = '<i class="fas fa-sync-alt"></i><span>자동회전</span>';
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 = '';
}