diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 485bc5b..3d73aad 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,9 @@ "Bash(sed:*)", "Bash(sort:*)", "WebFetch(domain:studio.v-llage.com)", - "Bash(awk:*)" + "Bash(awk:*)", + "Bash(python:*)", + "Bash(start http://localhost:8000/gallery.html)" ], "deny": [], "ask": [] diff --git a/css/gallery.css b/css/gallery.css index bd187b3..efb69c9 100644 --- a/css/gallery.css +++ b/css/gallery.css @@ -412,7 +412,7 @@ 20%, 80% { opacity: 1; } } -/* 개선된 360도 이미지 전체화면 모달 */ +/* 새로운 360도 파노라마 모달 */ .panorama-modal { display: none; position: fixed; @@ -422,7 +422,7 @@ height: 100vh; background: rgba(0, 0, 0, 0.95); z-index: 2000; - cursor: grab; + backdrop-filter: blur(5px); } .panorama-modal.active { @@ -431,111 +431,153 @@ justify-content: center; } -.panorama-modal:active { - cursor: grabbing; -} - .panorama-modal-content { position: relative; - width: 90vw; - height: 70vh; - max-width: 1200px; + width: 95vw; + height: 90vh; + max-width: 1400px; border-radius: var(--border-radius); overflow: hidden; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.7); + background: #000; } -.panorama-modal-viewer { +/* 새로운 360도 뷰어 컨테이너 */ +.panorama-viewer-container { position: relative; width: 100%; height: 100%; overflow: hidden; border-radius: var(--border-radius); background: #000; + cursor: grab; } -.panorama-modal-image { +.panorama-viewer-container:active { + cursor: grabbing; +} + +/* 360도 뷰어 로딩 스피너 */ +.panorama-loader { position: absolute; - top: 0; - left: 0; - height: 100%; - object-fit: cover; - transition: transform 0.1s ease-out; - will-change: transform; - user-select: none; - pointer-events: none; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + color: var(--text-white); + z-index: 10; } +.panorama-loader .spinner { + width: 50px; + height: 50px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top: 3px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 15px; +} + +.panorama-loader p { + font-size: var(--font-base); + opacity: 0.8; + margin: 0; +} + +/* 새로운 모달 컨트롤 */ .panorama-modal-controls { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0, 0, 0, 0.9)); - padding: var(--spacing-xl); + padding: var(--spacing-xl) var(--spacing-2xl); display: flex; justify-content: space-between; align-items: center; + backdrop-filter: blur(10px); } .panorama-modal-title { color: var(--text-white); - font-weight: 600; - font-size: var(--font-xl); + font-weight: 700; + font-size: var(--font-2xl); + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); } -.panorama-modal-buttons { +/* 새로운 컨트롤 버튼 스타일 */ +.panorama-control-buttons { display: flex; - gap: var(--spacing-md); + gap: var(--spacing-lg); + flex-wrap: wrap; } -.panorama-modal-btn { - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); +.panorama-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-xs); + background: rgba(255, 255, 255, 0.15); + border: 2px solid rgba(255, 255, 255, 0.3); color: var(--text-white); - padding: var(--spacing-sm) var(--spacing-lg); + padding: var(--spacing-md) var(--spacing-lg); border-radius: var(--border-radius); cursor: pointer; - font-size: var(--font-base); - transition: var(--transition); - backdrop-filter: blur(10px); + font-size: var(--font-sm); font-weight: 500; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + min-width: 80px; } -.panorama-modal-btn:hover { +.panorama-btn:hover { background: var(--primary-color); border-color: var(--primary-color); - transform: translateY(-2px); + transform: translateY(-3px) scale(1.05); + box-shadow: 0 8px 20px rgba(255, 136, 0, 0.4); } -.panorama-modal-btn.active { +.panorama-btn.active { background: var(--primary-color); border-color: var(--primary-color); + box-shadow: 0 4px 15px rgba(255, 136, 0, 0.3); } +.panorama-btn i { + font-size: var(--font-lg); +} + +.panorama-btn span { + font-size: var(--font-xs); + text-align: center; +} + +/* 새로운 닫기 버튼 */ .panorama-modal-close { position: absolute; - top: var(--spacing-lg); - right: var(--spacing-lg); - background: rgba(0, 0, 0, 0.7); + top: var(--spacing-xl); + right: var(--spacing-xl); + background: rgba(0, 0, 0, 0.8); color: var(--text-white); - width: 50px; - height: 50px; + width: 60px; + height: 60px; border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.3); + border: 3px solid rgba(255, 255, 255, 0.3); cursor: pointer; display: flex; align-items: center; justify-content: center; - font-size: var(--font-xl); - transition: var(--transition); + font-size: var(--font-2xl); + transition: all 0.3s ease; backdrop-filter: blur(10px); + z-index: 100; } .panorama-modal-close:hover { background: var(--primary-color); border-color: var(--primary-color); - transform: scale(1.1); + transform: scale(1.1) rotate(90deg); + box-shadow: 0 8px 20px rgba(255, 136, 0, 0.4); } .panorama-modal-close::before { @@ -543,60 +585,63 @@ font-weight: bold; } -.panorama-modal-help-text { +/* 새로운 도움말 패널 */ +.panorama-help-panel { position: absolute; - top: var(--spacing-xl); - right: var(--spacing-xl); - background: rgba(0, 0, 0, 0.9); - color: var(--text-white); - padding: var(--spacing-lg); - border-radius: var(--border-radius); - font-size: var(--font-sm); - max-width: 250px; - z-index: 10; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - display: none; -} - -.panorama-modal-help-text p { - margin: 0 0 var(--spacing-xs) 0; -} - -.panorama-modal-help-text strong { - color: var(--primary-color); - font-size: var(--font-base); -} - -.panorama-modal-help { - position: absolute; - top: var(--spacing-lg); - left: var(--spacing-lg); - background: rgba(255, 136, 0, 0.9); - color: var(--text-white); - padding: var(--spacing-sm) var(--spacing-lg); - border-radius: var(--border-radius); - font-size: var(--font-base); - font-weight: 500; - animation: fadeInOut 4s ease-in-out; - backdrop-filter: blur(10px); -} - -.panorama-modal-indicator { - position: absolute; - top: var(--spacing-lg); + top: 50%; left: 50%; - transform: translateX(-50%); - background: rgba(0, 0, 0, 0.7); + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.95); color: var(--text-white); - padding: var(--spacing-sm) var(--spacing-lg); - border-radius: var(--border-radius-full); - font-size: var(--font-base); - font-weight: 600; - border: 2px solid rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); + padding: var(--spacing-2xl); + border-radius: var(--border-radius-lg); + max-width: 500px; + width: 90%; + z-index: 200; + backdrop-filter: blur(20px); + border: 2px solid rgba(255, 255, 255, 0.2); + display: none; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8); } +.panorama-help-panel h3 { + color: var(--primary-color); + font-size: var(--font-xl); + margin: 0 0 var(--spacing-lg) 0; + text-align: center; + font-weight: 700; +} + +.help-content { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.help-item { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-sm); + background: rgba(255, 255, 255, 0.05); + border-radius: var(--border-radius); + border-left: 3px solid var(--primary-color); +} + +.help-item i { + color: var(--primary-color); + font-size: var(--font-lg); + width: 24px; + text-align: center; +} + +.help-item span { + font-size: var(--font-base); + line-height: 1.5; +} + +/* 기존 스타일 제거됨 - 새로운 도움말 패널로 대체 */ + /* 반응형 디자인 */ @media (max-width: 768px) { .page-header h1 { @@ -709,36 +754,64 @@ } .panorama-modal-content { - width: 95vw; - height: 60vh; + width: 98vw; + height: 85vh; } .panorama-modal-controls { padding: var(--spacing-lg); + flex-direction: column; + gap: var(--spacing-md); + align-items: center; } .panorama-modal-title { + font-size: var(--font-lg); + text-align: center; + } + + .panorama-control-buttons { + gap: var(--spacing-sm); + justify-content: center; + } + + .panorama-btn { + min-width: 70px; + padding: var(--spacing-sm) var(--spacing-md); + } + + .panorama-btn i { font-size: var(--font-base); } - .panorama-modal-btn { - font-size: var(--font-sm); - padding: var(--spacing-xs) var(--spacing-md); + .panorama-btn span { + font-size: 10px; } .panorama-modal-close { - width: 40px; - height: 40px; - font-size: var(--font-base); + width: 50px; + height: 50px; + font-size: var(--font-lg); + top: var(--spacing-md); + right: var(--spacing-md); } - .panorama-modal-help { - font-size: var(--font-sm); - padding: var(--spacing-xs) var(--spacing-md); + .panorama-help-panel { + width: 95%; + padding: var(--spacing-lg); + max-width: none; } - .panorama-modal-indicator { + .panorama-help-panel h3 { + font-size: var(--font-lg); + } + + .help-item { + gap: var(--spacing-sm); + padding: var(--spacing-xs); + } + + .help-item span { font-size: var(--font-sm); - padding: var(--spacing-xs) var(--spacing-md); } } \ No newline at end of file diff --git a/gallery.html b/gallery.html index b87d8a4..6fa507d 100644 --- a/gallery.html +++ b/gallery.html @@ -40,8 +40,6 @@ - - diff --git a/js/gallery.js b/js/gallery.js index 3a1b63e..b15ecfe 100644 --- a/js/gallery.js +++ b/js/gallery.js @@ -6,7 +6,7 @@ document.addEventListener('DOMContentLoaded', function() { initGallery(); initLightbox(); initGalleryAnimations(); - initAFrame360Viewers(); + initCustom360Viewers(); }); // 갤러리 초기화 @@ -267,268 +267,605 @@ function handleSwipe() { } // ======================================== -// A-Frame 기반 360도 VR 뷰어 - 안정적이고 강력한 VR 체험 +// 간단한 360도 파노라마 뷰어 - 좌우 스크롤 방식 // ======================================== -let currentAFrameScene = null; -let autoRotationInterval = null; -let isAutoRotating = false; - -function initAFrame360Viewers() { - // 모달 생성 - createAFrameModal(); +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(); + } - // 미리보기 설정 - initAFramePreviews(); + 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 = ` +