mingle-website/js/gallery.js
68893236+KINDNICK@users.noreply.github.com b5008b2f5d Refactor: JS 버그 수정 23건 + 이미지 최적화 + 크리에이터 사인 추가
- JS 논리 오류 수정: gallery.js lightbox 초기화 타이밍, 터치 리스너 누적, IntersectionObserver 통합
- XSS 방지: qna.js showNoResults innerHTML → textContent, 정규식 이스케이프 추가
- 안전성 개선: popup.js ESC 가드, portfolio.js getIframe optional chaining, backgrounds/props null 가드
- 이미지 최적화: 스튜디오 12장 WebP 압축 (4.0MB → 2.2MB, 46% 감소)
- 360 이미지: git 히스토리에서 원본 복구 후 4096×2048 리사이즈 (해상도 4.6배 향상)
- 360 뷰어: image-rendering auto 전환, naturalWidth/Height 기반 렌더링으로 품질 개선
- 크리에이터 사인 추가: 얌하 (3.3KB), 구슬요 (5.9KB) WebP 변환 및 마키 삽입
- 불필요 코드 제거: gallery.js 미사용 함수 6개 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:56:48 +09:00

908 lines
32 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');
// 레이지 로딩용 옵저버를 하나만 생성 (이미지마다 생성하지 않음)
let imageObserver = null;
if ('IntersectionObserver' in window) {
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);
}
});
});
}
galleryItems.forEach((item, index) => {
const img = item.querySelector('.gallery-img');
if (!img) return;
// 이미지 클릭 시 라이트박스 열기
img.addEventListener('click', () => openLightbox(index));
// 이미지 로딩 에러 처리
img.addEventListener('error', function() {
this.src = 'images/placeholder.jpg';
this.alt = '이미지를 불러올 수 없습니다';
});
// 레이지 로딩 구현
if (imageObserver && img.dataset.src) {
imageObserver.observe(img);
}
});
}
// 라이트박스 기능
let currentImageIndex = 0;
let galleryImages = [];
function initLightbox() {
// DOM 준비 후 갤러리 이미지 수집
galleryImages = document.querySelectorAll('.gallery-img');
// 라이트박스 HTML 생성
const lightboxHTML = `
<div id="lightbox" class="lightbox" role="dialog" aria-label="이미지 뷰어">
<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()" aria-label="닫기"></button>
<button class="lightbox-nav lightbox-prev" onclick="previousImage()" aria-label="이전 이미지"></button>
<button class="lightbox-nav lightbox-next" onclick="nextImage()" aria-label="다음 이미지"></button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', lightboxHTML);
// ESC 키로 라이트박스 닫기 (라이트박스가 열려 있을 때만)
document.addEventListener('keydown', function(e) {
const lightbox = document.getElementById('lightbox');
if (!lightbox || !lightbox.classList.contains('active')) return;
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();
});
}
let lightboxLastFocused = null;
function openLightbox(index) {
currentImageIndex = index;
lightboxLastFocused = document.activeElement;
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxCaption = document.getElementById('lightbox-caption');
const currentImg = galleryImages[index];
if (!currentImg) return;
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');
// iOS 스크롤 방지
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
document.body.style.top = `-${window.scrollY}px`;
// 포커스를 닫기 버튼으로
const closeBtn = lightbox.querySelector('.lightbox-close');
if (closeBtn) closeBtn.focus();
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('active');
const scrollY = document.body.style.top;
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
document.body.style.top = '';
window.scrollTo(0, parseInt(scrollY || '0') * -1);
if (lightboxLastFocused) lightboxLastFocused.focus();
}
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);
});
}
// 터치 이벤트 지원 (모바일) - 라이트박스에만 스코프 제한
// 리스너는 한 번만 등록하고, 라이트박스 활성 상태에서만 동작
let lightboxTouchStartX = 0;
let lightboxTouchListenersAdded = false;
function setupLightboxTouchListeners() {
if (lightboxTouchListenersAdded) return;
const lightbox = document.getElementById('lightbox');
if (!lightbox) return;
lightboxTouchListenersAdded = true;
lightbox.addEventListener('touchstart', function(e) {
if (!lightbox.classList.contains('active')) return;
lightboxTouchStartX = e.changedTouches[0].screenX;
});
lightbox.addEventListener('touchend', function(e) {
if (!lightbox.classList.contains('active')) return;
const touchEndX = e.changedTouches[0].screenX;
const swipeThreshold = 50;
const diff = lightboxTouchStartX - touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
nextImage();
} else {
previousImage();
}
}
});
}
// openLightbox 래핑: 터치 리스너 초기화 보장
const _originalOpenLightbox = openLightbox;
openLightbox = function(index) {
_originalOpenLightbox(index);
setupLightboxTouchListeners();
};
// ========================================
// 간단한 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;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
touch-action: 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;
transform-origin: left center;
backface-visibility: hidden;
`;
// 로딩 표시
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;
image-rendering: auto;
filter: contrast(1.05) saturate(1.1);
`;
this.image.draggable = false;
// 이미지 로드 완료 후 - 안정성 개선
this.image.onload = () => {
// 이미지가 완전히 로드될 때까지 대기
setTimeout(() => {
this.hideLoading();
this.setupImageSize();
this.createDuplicateImages();
this.updateTransform();
}, 100);
};
this.image.onerror = () => {
this.hideLoading();
this.showError();
};
this.imageWrapper.appendChild(this.image);
this.container.appendChild(this.imageWrapper);
}
setupImageSize() {
// 컨테이너 크기 가져오기
const containerHeight = this.container.clientHeight;
const containerWidth = this.container.clientWidth;
const naturalW = this.image.naturalWidth;
const naturalH = this.image.naturalHeight;
const imageAspectRatio = naturalW / naturalH;
// 컨테이너 높이에 맞추되, 원본 해상도를 초과하지 않도록 제한
let imageHeight = Math.min(containerHeight * this.zoom, naturalH);
let imageWidth = imageHeight * imageAspectRatio;
// 최소 너비 보장: 컨테이너 1.5배 (3배까지 늘리면 흐릿해짐)
// 단, 원본 너비를 초과하지 않도록 제한
const minWidth = Math.min(containerWidth * 1.5, naturalW);
if (imageWidth < minWidth) {
imageWidth = minWidth;
imageHeight = imageWidth / imageAspectRatio;
}
// 줌 레벨에 따른 크기 조정
if (this.zoom > 1) {
imageHeight = Math.max(containerHeight, imageHeight);
imageWidth = imageHeight * imageAspectRatio;
}
// 이미지 크기 설정 - 픽셀 완벽 정렬로 경계 문제 방지
this.image.style.width = Math.round(imageWidth) + 'px';
this.image.style.height = Math.round(imageHeight) + 'px';
this.imageWrapper.style.width = Math.round(imageWidth) + 'px';
this.imageWrapper.style.height = Math.round(containerHeight) + 'px';
// 단일 이미지 너비 저장
this.singleImageWidth = imageWidth;
}
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;
imageClone1.src = this.image.src;
imageClone2.src = this.image.src;
imageClone1.draggable = false;
imageClone2.draggable = false;
this.imageWrapper.appendChild(imageClone1);
this.imageWrapper.appendChild(imageClone2);
// 래퍼 전체 너비를 3배로 설정
this.imageWrapper.style.width = (this.singleImageWidth * 3) + 'px';
// 시작 위치를 중앙 이미지로 (두 번째 이미지)
this.currentX = this.singleImageWidth;
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() {
// 바인딩된 핸들러 참조 저장 (cleanup 용)
this._boundHandleStart = this.handleStart.bind(this);
this._boundHandleMove = this.handleMove.bind(this);
this._boundHandleEnd = this.handleEnd.bind(this);
this._boundHandleWheel = this.handleWheel.bind(this);
this._boundHandleResize = this.handleResize.bind(this);
// 마우스 이벤트
this.container.addEventListener('mousedown', this._boundHandleStart);
this.container.addEventListener('mousemove', this._boundHandleMove);
this.container.addEventListener('mouseup', this._boundHandleEnd);
this.container.addEventListener('mouseleave', this._boundHandleEnd);
// 터치 이벤트
this.container.addEventListener('touchstart', this._boundHandleStart, { passive: false });
this.container.addEventListener('touchmove', this._boundHandleMove, { passive: false });
this.container.addEventListener('touchend', this._boundHandleEnd);
// 휠 이벤트 (좌우 스크롤)
this.container.addEventListener('wheel', this._boundHandleWheel, { passive: false });
// 컨텍스트 메뉴 방지
this.container.addEventListener('contextmenu', e => e.preventDefault());
// 리사이즈
window.addEventListener('resize', this._boundHandleResize);
}
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.15;
// 극한 속도 제한 (튀는 현상 방지)
const maxVelocity = this.singleImageWidth * 0.05;
this.velocity = Math.max(-maxVelocity, Math.min(maxVelocity, this.velocity));
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) {
// 리사이즈 시 비율 유지하되, 튀는 현상 방지
const oldRatio = (this.currentX - this.singleImageWidth) / this.singleImageWidth;
const oldImageWidth = this.singleImageWidth;
this.setupImageSize();
this.createDuplicateImages(); // 이미지 다시 생성
// 정규화된 위치로 복원 (중앙 이미지 기준)
this.currentX = this.singleImageWidth + (this.singleImageWidth * oldRatio);
// 경계값 확인 및 보정
if (this.currentX < 0) this.currentX += this.singleImageWidth;
if (this.currentX > this.singleImageWidth * 2) this.currentX -= this.singleImageWidth;
this.updateTransform();
}
}
updateTransform() {
if (!this.imageWrapper || !this.singleImageWidth) return;
// 무한 스크롤 처리 - 튀는 버그 방지를 위한 부드러운 전환
const threshold = this.singleImageWidth * 0.1; // 10% 여유 공간
// 왼쪽 경계 처리 - 부드러운 전환
if (this.currentX < -threshold) {
this.currentX = this.singleImageWidth + (this.currentX + threshold);
}
// 오른쪽 경계 처리 - 부드러운 전환
else if (this.currentX > this.singleImageWidth * 2 + threshold) {
this.currentX = this.singleImageWidth + (this.currentX - this.singleImageWidth * 2 - threshold);
}
// 정확한 무한 루프 경계 처리 (튀는 현상 방지)
const normalizedX = ((this.currentX % this.singleImageWidth) + this.singleImageWidth) % this.singleImageWidth;
const actualX = normalizedX + this.singleImageWidth;
// 현재 위치와 목표 위치의 차이가 큰 경우만 보정 (부드러운 회전 유지)
if (Math.abs(this.currentX - actualX) > this.singleImageWidth * 0.5) {
this.currentX = actualX;
}
// 고정밀 변환 계산 (소수점 2자리까지 유지하여 부드러움 보장)
const translateX = Math.round(-this.currentX * 100) / 100;
const scale = Math.round(this.zoom * 1000) / 1000;
const transform = `translateX(${translateX}px) scale(${scale})`;
this.imageWrapper.style.transform = transform;
// 브라우저 호환성
this.imageWrapper.style.webkitTransform = transform;
this.imageWrapper.style.msTransform = 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 = this.singleImageWidth; // 중앙 이미지로
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._boundHandleResize);
if (this.container) {
this.container.removeEventListener('mousedown', this._boundHandleStart);
this.container.removeEventListener('mousemove', this._boundHandleMove);
this.container.removeEventListener('mouseup', this._boundHandleEnd);
this.container.removeEventListener('mouseleave', this._boundHandleEnd);
this.container.removeEventListener('touchstart', this._boundHandleStart);
this.container.removeEventListener('touchmove', this._boundHandleMove);
this.container.removeEventListener('touchend', this._boundHandleEnd);
this.container.removeEventListener('wheel', this._boundHandleWheel);
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">
<div class="close-icon"></div>
</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');
// iOS 스크롤 방지
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
document.body.style.top = `-${window.scrollY}px`;
// 간단한 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');
const scrollY = document.body.style.top;
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
document.body.style.top = '';
window.scrollTo(0, parseInt(scrollY || '0') * -1);
// 컨테이너 정리
document.getElementById('panorama-viewer-container').innerHTML = '';
}