Fix : 갤러리 업데이트

This commit is contained in:
qsxft258@gmail.com 2025-09-17 22:18:02 +09:00
parent e50cfa698f
commit 243e3b3bc0
4 changed files with 705 additions and 295 deletions

View File

@ -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": []

View File

@ -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);
}
}

View File

@ -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">

View File

@ -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 = `
<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 = '';
}
}
}
function initAFramePreviews() {
const containers = document.querySelectorAll('.panorama-clickable');
let current360Viewer = null;
function initCustom360Viewers() {
createPanoramaModal();
initPanoramaPreviews();
}
function initPanoramaPreviews() {
const clickableElements = document.querySelectorAll('.panorama-clickable');
containers.forEach(container => {
const imageSrc = container.dataset.image;
const title = container.dataset.title;
clickableElements.forEach(element => {
const imageSrc = element.dataset.image;
const title = element.dataset.title;
// 클릭 이벤트
container.addEventListener('click', () => {
openAFrameModal(imageSrc, 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 (isAutoRotating) {
stopAutoRotation();
autoBtn.innerHTML = '<i class="fas fa-sync-alt"></i> 자동회전';
autoBtn.classList.remove('active');
} else {
startAutoRotation();
autoBtn.innerHTML = '<i class="fas fa-pause"></i> 정지';
autoBtn.classList.add('active');
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;
}
}
});
// 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
}
function stopAutoRotation() {
isAutoRotating = false;
if (autoRotationInterval) {
clearInterval(autoRotationInterval);
autoRotationInterval = null;
// WebGL 뷰어 정리
if (current360Viewer) {
current360Viewer.destroy();
current360Viewer = 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 = '';
}