Fix : 갤러리 업데이트
This commit is contained in:
parent
e50cfa698f
commit
243e3b3bc0
@ -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": []
|
||||
|
||||
285
css/gallery.css
285
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);
|
||||
}
|
||||
}
|
||||
@ -40,8 +40,6 @@
|
||||
<!-- 아이콘 폰트 (이모지 대체용) -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- A-Frame VR Framework -->
|
||||
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="css/common.css">
|
||||
<link rel="stylesheet" href="css/gallery.css">
|
||||
|
||||
695
js/gallery.js
695
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;
|
||||
class Easy360Viewer {
|
||||
constructor(container, imageSrc, title) {
|
||||
this.container = container;
|
||||
this.imageSrc = imageSrc;
|
||||
this.title = title;
|
||||
|
||||
function initAFrame360Viewers() {
|
||||
// 모달 생성
|
||||
createAFrameModal();
|
||||
// 뷰어 상태
|
||||
this.currentX = 0; // 현재 X 위치
|
||||
this.maxX = 0; // 최대 스크롤 가능 X
|
||||
this.zoom = 1;
|
||||
|
||||
// 미리보기 설정
|
||||
initAFramePreviews();
|
||||
// 마우스/터치 상태
|
||||
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();
|
||||
}
|
||||
|
||||
function initAFramePreviews() {
|
||||
const containers = document.querySelectorAll('.panorama-clickable');
|
||||
init() {
|
||||
this.createViewer();
|
||||
this.bindEvents();
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
containers.forEach(container => {
|
||||
const imageSrc = container.dataset.image;
|
||||
const title = container.dataset.title;
|
||||
createViewer() {
|
||||
// 컨테이너 스타일
|
||||
this.container.style.cssText = `
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
// 클릭 이벤트
|
||||
container.addEventListener('click', () => {
|
||||
openAFrameModal(imageSrc, title);
|
||||
// 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 help = container.querySelector('.panorama-help');
|
||||
if (help) {
|
||||
help.style.opacity = '0';
|
||||
setTimeout(() => help.remove(), 1000);
|
||||
const helpText = element.querySelector('.panorama-help');
|
||||
if (helpText) {
|
||||
helpText.style.opacity = '0';
|
||||
setTimeout(() => helpText.remove(), 500);
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
}
|
||||
|
||||
function createAFrameModal() {
|
||||
function createPanoramaModal() {
|
||||
const modalHTML = `
|
||||
<div id="aframe-modal" class="panorama-modal">
|
||||
<div id="panorama-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 id="panorama-viewer-container" class="panorama-viewer-container">
|
||||
<!-- 360도 뷰어가 여기에 들어갑니다 -->
|
||||
</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> 리셋
|
||||
<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-modal-btn" id="aframe-auto-btn">
|
||||
<i class="fas fa-sync-alt"></i> 자동회전
|
||||
<button class="panorama-btn" id="panorama-auto-btn">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
<span>자동회전</span>
|
||||
</button>
|
||||
<button class="panorama-modal-btn" id="aframe-vr-btn">
|
||||
<i class="fas fa-vr-cardboard"></i> VR모드
|
||||
<button class="panorama-btn" id="panorama-zoom-in-btn">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
<span>확대</span>
|
||||
</button>
|
||||
<button class="panorama-modal-btn" id="aframe-help-btn">
|
||||
<i class="fas fa-question-circle"></i> 도움말
|
||||
<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="aframe-modal-close">
|
||||
|
||||
<button class="panorama-modal-close" id="panorama-close-btn">
|
||||
<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 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);
|
||||
setupAFrameModalListeners();
|
||||
setupPanoramaModalEvents();
|
||||
}
|
||||
|
||||
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');
|
||||
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 helpVisible = false;
|
||||
let isAutoRotating = false;
|
||||
let isHelpVisible = false;
|
||||
|
||||
// 모달 닫기
|
||||
closeBtn.addEventListener('click', closeAFrameModal);
|
||||
closeBtn.addEventListener('click', closePanoramaModal);
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeAFrameModal();
|
||||
if (e.target === modal) closePanoramaModal();
|
||||
});
|
||||
|
||||
// ESC 키
|
||||
// ESC 키로 닫기
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||||
closeAFrameModal();
|
||||
closePanoramaModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 리셋 버튼
|
||||
// 초기화 버튼
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (currentAFrameScene) {
|
||||
const camera = currentAFrameScene.querySelector('[camera]');
|
||||
if (camera) {
|
||||
camera.setAttribute('rotation', '0 0 0');
|
||||
camera.setAttribute('position', '0 0 0');
|
||||
}
|
||||
if (current360Viewer) {
|
||||
current360Viewer.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// 자동회전 버튼
|
||||
autoBtn.addEventListener('click', () => {
|
||||
if (current360Viewer) {
|
||||
if (isAutoRotating) {
|
||||
stopAutoRotation();
|
||||
autoBtn.innerHTML = '<i class="fas fa-sync-alt"></i> 자동회전';
|
||||
current360Viewer.stopAutoRotate();
|
||||
autoBtn.innerHTML = '<i class="fas fa-sync-alt"></i><span>자동회전</span>';
|
||||
autoBtn.classList.remove('active');
|
||||
isAutoRotating = false;
|
||||
} else {
|
||||
startAutoRotation();
|
||||
autoBtn.innerHTML = '<i class="fas fa-pause"></i> 정지';
|
||||
current360Viewer.startAutoRotate();
|
||||
autoBtn.innerHTML = '<i class="fas fa-pause"></i><span>정지</span>';
|
||||
autoBtn.classList.add('active');
|
||||
isAutoRotating = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// VR 버튼
|
||||
vrBtn.addEventListener('click', () => {
|
||||
if (currentAFrameScene) {
|
||||
const vrButton = currentAFrameScene.querySelector('[vr-mode-ui]');
|
||||
if (vrButton) {
|
||||
// VR 모드 진입
|
||||
currentAFrameScene.enterVR();
|
||||
// 확대 버튼
|
||||
zoomInBtn.addEventListener('click', () => {
|
||||
if (current360Viewer) {
|
||||
current360Viewer.zoomIn();
|
||||
}
|
||||
});
|
||||
|
||||
// 축소 버튼
|
||||
zoomOutBtn.addEventListener('click', () => {
|
||||
if (current360Viewer) {
|
||||
current360Viewer.zoomOut();
|
||||
}
|
||||
});
|
||||
|
||||
// 도움말 버튼
|
||||
helpBtn.addEventListener('click', () => {
|
||||
helpVisible = !helpVisible;
|
||||
helpText.style.display = helpVisible ? 'block' : 'none';
|
||||
helpBtn.classList.toggle('active', helpVisible);
|
||||
isHelpVisible = !isHelpVisible;
|
||||
helpPanel.style.display = isHelpVisible ? 'block' : 'none';
|
||||
helpBtn.classList.toggle('active', isHelpVisible);
|
||||
});
|
||||
|
||||
// 도움말 자동 숨김
|
||||
setTimeout(() => {
|
||||
helpText.style.display = 'none';
|
||||
}, 6000);
|
||||
// 도움말 패널 클릭시 닫기
|
||||
helpPanel.addEventListener('click', (e) => {
|
||||
if (e.target === helpPanel) {
|
||||
isHelpVisible = false;
|
||||
helpPanel.style.display = 'none';
|
||||
helpBtn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openAFrameModal(imageSrc, title) {
|
||||
const modal = document.getElementById('aframe-modal');
|
||||
const modalTitle = document.getElementById('aframe-modal-title');
|
||||
const viewerContainer = document.getElementById('aframe-viewer');
|
||||
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';
|
||||
|
||||
// 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);
|
||||
});
|
||||
// 간단한 360도 좌우 스크롤 뷰어 생성
|
||||
current360Viewer = new Easy360Viewer(viewerContainer, imageSrc, title);
|
||||
}
|
||||
|
||||
function startAutoRotation() {
|
||||
if (!currentAFrameScene) return;
|
||||
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');
|
||||
|
||||
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
|
||||
// WebGL 뷰어 정리
|
||||
if (current360Viewer) {
|
||||
current360Viewer.destroy();
|
||||
current360Viewer = null;
|
||||
}
|
||||
|
||||
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> 자동회전';
|
||||
// UI 초기화
|
||||
autoBtn.innerHTML = '<i class="fas fa-sync-alt"></i><span>자동회전</span>';
|
||||
autoBtn.classList.remove('active');
|
||||
|
||||
// A-Frame Scene 정리
|
||||
if (currentAFrameScene) {
|
||||
currentAFrameScene.destroy();
|
||||
currentAFrameScene = null;
|
||||
}
|
||||
helpBtn.classList.remove('active');
|
||||
helpPanel.style.display = 'none';
|
||||
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// 컨테이너 정리
|
||||
document.getElementById('aframe-viewer').innerHTML = '';
|
||||
document.getElementById('panorama-viewer-container').innerHTML = '';
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user