This commit is contained in:
msbfox
2026-06-14 10:39:56 +09:00
parent 4ee992e855
commit 0345b8cd08
514 changed files with 492 additions and 953 deletions
@@ -79,7 +79,7 @@ function initProducts(productsData) {
preview.classList.add('fixed-mode');
preview.style.display = 'flex';
};
hotspotLayer.appendChild(hs);
// hotspotLayer.appendChild(hs);
});
@@ -1,56 +0,0 @@
function initProducts(productsData) {
const map = document.getElementById('main-map');
const hotspotLayer = document.getElementById('hotspot-layer');
const preview = document.getElementById('hover-preview');
const previewGrid = document.getElementById('preview-grid');
// const coordDisplay = document.getElementById('coord-display');
// 그룹화 로직
const groups = {};
productsData.forEach(p => {
const key = `${parseFloat(p.xPct).toFixed(1)}_${parseFloat(p.yPct).toFixed(1)}`;
if (!groups[key]) groups[key] = [];
groups[key].push(p);
});
Object.keys(groups).forEach(key => {
const group = groups[key];
const hs = document.createElement('div');
hs.className = 'hotspot';
hs.style.left = group[0].xPct + '%';
hs.style.top = group[0].yPct + '%';
hs.style.transform = 'translate(-50%, -50%)';
hs.onmouseenter = () => {
preview.style.display = 'block';
previewGrid.innerHTML = group.map(p => `
<div class="product-item">
<div class="product-img-box"><img src="${p.img_url}" onerror="this.src='https://via.placeholder.com/200x120?text=No+Image'"></div>
<div class="text-[10px] text-blue-400 font-bold mb-1">${p.page}</div>
<h4 class="text-white text-[13px] font-bold truncate">${p.title}</h4>
<p class="text-slate-500 text-[10px] line-clamp-2">${p.desc}</p>
</div>
`).join('');
};
hs.onmouseleave = () => preview.style.display = 'none';
hs.onmousemove = (e) => {
let x = e.clientX + 30;
let y = e.clientY + 30;
if (x + 540 > window.innerWidth) x = e.clientX - 560;
if (y + preview.offsetHeight > window.innerHeight) y = window.innerHeight - preview.offsetHeight - 20;
preview.style.left = x + 'px';
preview.style.top = y + 'px';
};
hotspotLayer.appendChild(hs);
});
// 좌표 도구
// map.onmousemove = (e) => {
// const r = map.getBoundingClientRect();
// coordDisplay.textContent = `X: ${((e.clientX-r.left)/r.width*100).toFixed(1)}%, Y: ${((e.clientY-r.top)/r.height*100).toFixed(1)}%`;
// };
// map.onclick = (e) => {
// const r = map.getBoundingClientRect();
// prompt("좌표:", `xPct: ${((e.clientX-r.left)/r.width*100).toFixed(1)}, yPct: ${((e.clientY-r.top)/r.height*100).toFixed(1)}`);
// };
}
@@ -1,103 +0,0 @@
function initProducts(productsData) {
const map = document.getElementById('main-map');
const hotspotLayer = document.getElementById('hotspot-layer');
const preview = document.getElementById('hover-preview');
const previewGrid = document.getElementById('preview-grid');
// const coordDisplay = document.getElementById('coord-display');
let isFixed = false; // 팝업 고정 상태 변수
const groups = {};
productsData.forEach(p => {
const key = `${parseFloat(p.x_pct || p.xPct).toFixed(1)}_${parseFloat(p.y_pct || p.yPct).toFixed(1)}`;
if (!groups[key]) groups[key] = [];
groups[key].push(p);
});
Object.keys(groups).forEach(key => {
const group = groups[key];
const hs = document.createElement('div');
hs.className = 'hotspot';
hs.style.left = (group[0].x_pct || group[0].xPct) + '%';
hs.style.top = (group[0].y_pct || group[0].yPct) + '%';
hs.style.transform = 'translate(-50%, -50%)';
// 팝업 내용 생성 함수
const updateContent = () => {
const gridClass = group.length === 1 ? 'single-item' : 'multi-items';
previewGrid.className = `products-grid ${gridClass}`;
previewGrid.innerHTML = group.map(p => `
<div class="product-item">
<div class="product-img-box"><img src="${p.img_url}" onerror="this.src='https://via.placeholder.com/200x120?text=No+Image'"></div>
<div class="info-content">
<div class="text-[11px] text-blue-400 font-bold mb-1">${p.page}</div>
<h4 class="text-white text-[16px] font-bold mb-2">${p.title}</h4>
<p class="text-slate-400 text-[13px] leading-relaxed">${p.desc}</p>
</div>
</div>
`).join('');
};
// 마우스 진입 (호버 시작)
hs.onmouseenter = (e) => {
if (isFixed) return;
updateContent();
preview.style.display = 'block';
};
// 마우스 이동 (팝업이 마우스를 따라다님)
hs.onmousemove = (e) => {
if (isFixed) return;
let x = e.clientX + 20;
let y = e.clientY - (preview.offsetHeight / 2);
if (x + 560 > window.innerWidth) x = e.clientX - 580;
if (y < 20) y = 20;
preview.style.left = x + 'px';
preview.style.top = y + 'px';
};
// 마우스 이탈 (호버 종료)
hs.onmouseleave = () => {
if (isFixed) return;
preview.style.display = 'none';
};
// 클릭 시 고정
hs.onclick = (e) => {
e.stopPropagation();
isFixed = true;
updateContent();
preview.style.display = 'block';
// 고정 시에는 화면 중앙 근처 적절한 위치에 배치
let x = e.clientX + 30;
let y = e.clientY - (preview.offsetHeight / 2);
if (x + 720 > window.innerWidth) x = e.clientX - 730;
if (y < 20) y = 20;
preview.style.left = x + 'px';
preview.style.top = y + 'px';
preview.classList.add('fixed-mode');
};
hotspotLayer.appendChild(hs);
});
// 외부 클릭 시 고정 해제 및 닫기
document.addEventListener('click', (e) => {
if (!preview.contains(e.target)) {
isFixed = false;
preview.style.display = 'none';
preview.classList.remove('fixed-mode');
}
});
// // 좌표 도구
// map.onmousemove = (e) => {
// const r = map.getBoundingClientRect();
// coordDisplay.textContent = `X: ${((e.clientX-r.left)/r.width*100).toFixed(1)}%, Y: ${((e.clientY-r.top)/r.height*100).toFixed(1)}%`;
// };
// map.onclick = (e) => {
// const r = map.getBoundingClientRect();
// prompt("좌표:", `xPct: ${((e.clientX-r.left)/r.width*100).toFixed(1)}, yPct: ${((e.clientY-r.top)/r.height*100).toFixed(1)}`);
// };
}
@@ -1,16 +1,24 @@
/**
* [최종 통합본] 제품 사이드 메뉴 및 메인 맵 연동 모듈 (products2.js)
* 1: 중앙팝업, 2: 점선연결, 3: 제자리확대, 4: 1초 지연 후 중앙팝업
* 5: 점선연결 + 1초후 중앙팝업
* 1: 중앙팝업, 2: 점선연결, 3: 제자리확대, 4: 1초 지연 후 중앙팝업, 5: 팝업 박스 표시, 6: 점선연결 + 팝업 박스 표시
*/
(function() {
// 💡 설정: 원하는 모드 번호를 입력하세요.
const POPUP_MODE = 5;
const POPUP_MODE = 6; // 6번 모드로 설정합니다.
// 💡 설정: 팝업 박스에 표시할 내용의 타입을 선택하세요 ('text' 또는 'image').
const POPUP_CONTENT_DISPLAY_TYPE = 'image'; // 'text' 또는 'image'로 변경하여 테스트
let popupTimer = null; // 4번 모드용 타이머
let hoverTimeout = null; // 호버 인텐트 지연 타이머
const HOVER_DELAY = 50; // 마우스가 완전히 벗어났다고 판단할 지연 시간 (밀리초)
const MAX_MOVE_DISTANCE = 10; // 팝업이 핫스팟에서 이동할 수 있는 최대 거리 (px)
// 모든 효과와 타이머를 제거하는 청소기 함수 (popupBox 숨기는 로직은 제외)
function clearAllPopupsContent() { // 함수 이름 변경: 내용만 초기화
console.log('clearAllPopupsContent called!');
// console.trace(); // 이제 제거해도 됩니다.
// 모든 효과와 타이머를 제거하는 청소기 함수
function clearAllPopups() {
// 실행 중인 타이머가 있으면 취소
if (popupTimer) {
clearTimeout(popupTimer);
@@ -35,12 +43,24 @@
img.style.transform = '';
}
});
// popupContent 내용 및 스타일 초기화 (popupBox 자체는 건드리지 않음)
const popupBox = document.getElementById('popup-box');
if (popupBox) {
const popupContent = popupBox.querySelector('.content');
if (popupContent) {
popupContent.innerHTML = ''; // 내용 비우기
popupContent.style.cssText = ''; // 모든 인라인 스타일 초기화
}
}
}
function initProductEvents() {
const productItems = document.querySelectorAll('.product-section .product-item');
const mainArea = document.querySelector('.product-main');
const section = document.querySelector('.product-section');
const popupBox = document.getElementById('popup-box');
const popupContent = popupBox ? popupBox.querySelector('.content') : null;
if (!productItems.length || !mainArea) return;
@@ -48,24 +68,262 @@
const imgElement = item.querySelector('.product-img img');
const imgSrc = item.dataset.img || (imgElement ? imgElement.src : '');
const proLink = item.dataset.prolink;
const x1 = parseFloat(item.dataset.x1);
const y1 = parseFloat(item.dataset.y1);
// Get product details from data attributes
const title = item.dataset.title;
const desc = item.dataset.desc;
const content = item.dataset.content;
item.addEventListener('mouseenter', function() {
clearAllPopups(); // 새로 시작하기 전 초기화
console.log('Mouse entered product-item:', title);
const x1 = this.dataset.x1;
const y1 = this.dataset.y1;
// 섹션 숨김 지연 타이머가 있다면 취소 (호버 깜빡임 방지)
if (hoverTimeout) {
console.log('Clearing hoverTimeout on mouseenter.');
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
if (x1 && y1) {
// 1. 핫스팟(블루 스폿)은 어떤 모드든 즉시 표시
const hotspot = document.querySelector('.product-hotspot');
if (hotspot) {
hotspot.style.left = x1 + '%';
hotspot.style.top = y1 + '%';
hotspot.style.display = 'block';
// 새로운 팝업을 띄우기 전에 이전 팝업의 내용만 초기화
clearAllPopupsContent();
// 팝업 박스를 명시적으로 보이는 상태로 설정 (애니메이션 시작 전)
if (popupBox) {
gsap.killTweensOf(popupBox); // 혹시 모를 숨김 애니메이션 중단
popupBox.style.display = 'block';
// 애니메이션 초기 상태 설정
popupBox.style.width = '0px';
popupBox.style.height = '0px';
popupBox.style.opacity = '0';
popupBox.style.transform = 'none'; // transform 제거
// mainRect는 popupBox를 포함하는 product-main의 크기 정보
// 핫스팟 위치에 따라 팝업의 초기 시작 지점 설정
const isLeft = this.closest('.product-side').classList.contains('left');
const rect = item.getBoundingClientRect();//this.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const startX = isLeft ? rect.right : rect.left;
const startY = rect.bottom + (rect.height / 2); //rect.bottom + (rect.height / 2);
if (isLeft) {
// 핫스팟이 왼쪽에 있으면 팝업은 mainArea의 좌측 상단에서 시작
popupBox.style.left = startX+'px';
popupBox.style.top = startY+'px';
} else {
// 핫스팟이 오른쪽에 있으면 팝업은 mainArea의 우측 상단에서 시작
popupBox.style.left = startX +'px';
popupBox.style.top = startY +'px';
}
}
if (!isNaN(x1) && !isNaN(y1)) {
// 1. 핫스팟(블루 스폿)은 어떤 모드든 즉시 표시
if(x1 && y1) {
const hotspot = document.querySelector('.product-hotspot');
if (hotspot) {
hotspot.style.left = x1 + '%';
hotspot.style.top = y1 + '%';
hotspot.style.display = 'block';
}
}
// 2. 모드별 효과 실행
if (POPUP_MODE === 4) {
if (POPUP_MODE === 5 || POPUP_MODE === 6) {
if (popupBox && popupContent) {
// Set popup dimensions
const popupWidth = 600; // 요청하신 대로 600
const popupHeight = 500; // 요청하신 대로 500
const POPUP_HORIZONTAL_OFFSET = 20; // 핫스팟과 팝업 사이의 가로 간격 (px)
// popupContent의 기본 스타일 설정 (매우 중요!)
popupContent.style.width = '100%';
popupContent.style.height = '100%';
popupContent.style.display = 'flex';
popupContent.style.alignItems = 'center';
popupContent.style.justifyContent = 'center';
popupContent.style.color = '#fff'; // 기본 텍스트 색상
popupContent.style.overflow = 'hidden'; // 이 줄을 추가합니다.
popupContent.style.padding = '0'; // 이 줄을 추가합니다.
// Populate popup content based on POPUP_CONTENT_DISPLAY_TYPE
if (POPUP_CONTENT_DISPLAY_TYPE === 'image' && imgElement) {
popupContent.innerHTML = ''; // Clear previous content
popupContent.style.background = 'none'; // 이미지 표시 시 배경 제거
const clone = imgElement.cloneNode(true);
clone.src = imgSrc;
// clone.id = 'active-popup-img'; // 이 줄을 제거합니다!
clone.style.width = '100%'; // Make image fit popup box
clone.style.height = '100%';
clone.style.objectFit = 'contain'; // 'cover'에서 'contain'으로 변경
// clone.style.cursor = proLink ? 'pointer' : 'default'; // 이 줄을 주석 처리하거나 제거합니다.
clone.style.pointerEvents = 'none'; // 이미지 위에서 마우스 이벤트 방지
if (proLink) {
clone.onclick = () => { location.href = proLink; };
}
// console.log('--- Debugging appendChild ---');
// console.log('proLink:', proLink);
// console.log('Clone element:', clone);
// console.log('popupContent BEFORE append:', popupContent.innerHTML);
// console.log('popupContent.style.cssText BEFORE append:', popupContent.style.cssText); // 스타일 확인
popupContent.appendChild(clone);
// console.log('popupContent AFTER append:', popupContent.innerHTML);
// console.log('popupContent.style.cssText AFTER append:', popupContent.style.cssText); // 스타일 확인
// console.log('popupContent children AFTER append:', popupContent.children.length);
// console.log('popupContent ', popupContent);
// console.log('popupBox ', popupBox);
// console.log('--- End Debugging ---');
} else { // Default to text content or if POPUP_CONTENT_DISPLAY_TYPE is 'text'
popupContent.innerHTML = `
<h3>${title}</h3>
<p>${desc}</p>
<p>${content}</p>
${proLink ? `<a href="${proLink}" style="color: white; text-decoration: underline;">자세히 보기</a>` : ''}
`;
// 텍스트 표시 시 배경 적용
popupContent.style.background = 'linear-gradient(135deg, #667eea, #764ba2)';
}
const isLeft = this.closest('.product-side').classList.contains('left');
const rect = item.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const startX = isLeft ? rect.right : rect.left;
const startY = rect.bottom + (rect.height / 2);
// const hotspotX_px = startX +popupWidth;
// const hotspotY_px = !isLeft ? startY+popupHeight : startY - popupHeight;
const corners = {
topLeft: {
x: rect.left ,
y: rect.top
},
topRight: {
x: rect.right ,
y: rect.top
},
bottomLeft: {
x: rect.left ,
y: rect.bottom
},
bottomRight: {
x: rect.right ,
y: rect.bottom
}
};
// const hotspotX_px = isLeft ? rect.right : rect.left - popupWidth -POPUP_HORIZONTAL_OFFSET ;
// const hotspotY_px = isLeft ? rect.top - rect.height :rect.y;
const hotspotX_px = isLeft ? 462.0833435058594 : 922.0833740234375 ;
const hotspotY_px = isLeft ? 174.66668701171875 :247.33334350585938;
console.log(isLeft);
console.log(mainRect);
console.log(rect);
console.log(hotspotX_px);
console.log(hotspotY_px);
// Determine if hotspot is on the left or right half of the mainArea
const isHotspotLeftHalf = x1 < 50;
// Calculate ideal raw targetLeft based on side
let idealTargetLeft_px;
if (isHotspotLeftHalf) {
// Hotspot on left, popup appears to its right
idealTargetLeft_px = hotspotX_px + POPUP_HORIZONTAL_OFFSET;
} else {
// Hotspot on right, popup appears to its left
idealTargetLeft_px = hotspotX_px - popupWidth - POPUP_HORIZONTAL_OFFSET;
}
// Calculate ideal raw targetTop (vertically centered with hotspot)
let idealTargetTop_px = hotspotY_px - (popupHeight / 2);
// Clamp ideal targetLeft and targetTop to stay within mainArea bounds
let clampedIdealTargetLeft_px = Math.max(0, Math.min(idealTargetLeft_px, mainRect.width - popupWidth));
let clampedIdealTargetTop_px = Math.max(0, Math.min(idealTargetTop_px, mainRect.height - popupHeight));
// Calculate actual final position, limiting movement distance
let finalTargetLeft_px = clampedIdealTargetLeft_px;
let finalTargetTop_px = clampedIdealTargetTop_px;
// Calculate distance from hotspot center to clamped ideal popup center
const hotspotCenterToPopupCenterX = clampedIdealTargetLeft_px + (popupWidth / 2) - hotspotX_px;
const hotspotCenterToPopupCenterY = clampedIdealTargetTop_px + (popupHeight / 2) - hotspotY_px;
const distance = Math.sqrt(
hotspotCenterToPopupCenterX * hotspotCenterToPopupCenterX +
hotspotCenterToPopupCenterY * hotspotCenterToPopupCenterY
);
if (distance > MAX_MOVE_DISTANCE) {
// If distance exceeds max, scale down the movement vector
const ratio = MAX_MOVE_DISTANCE / distance;
finalTargetLeft_px = hotspotX_px + (hotspotCenterToPopupCenterX * ratio) - (popupWidth / 2);
finalTargetTop_px = hotspotY_px + (hotspotCenterToPopupCenterY * ratio) - (popupHeight / 2);
// Re-clamp to ensure it stays within mainArea bounds after adjustment
finalTargetLeft_px = Math.max(0, Math.min(finalTargetLeft_px, mainRect.width - popupWidth));
finalTargetTop_px = Math.max(0, Math.min(finalTargetTop_px, mainRect.height - popupHeight));
}
// 팝업 박스를 보여주는 애니메이션 (크기, 위치, 투명도 동시 애니메이션)
gsap.to(popupBox, {
width: popupWidth, // 최종 너비
height: popupHeight, // 최종 높이
left: hotspotX_px,//finalTargetLeft_px, // 최종 왼쪽 위치
top: hotspotY_px,//finalTargetTop_px, // 최종 위쪽 위치
opacity: 1, // 최종 투명도
transform: 'none', // transform 속성 제거
duration: 1.5, // 요청하신 대로 1.5초로 변경
ease: "power2.out",
pointerEvents: 'auto',
onComplete: () => { // 이 onComplete 콜백을 추가합니다.
popupBox.style.opacity = '1'; // 애니메이션 완료 후 opacity를 1로 강제
popupBox.style.display = 'block'; // 애니메이션 완료 후 display를 block으로 강제
popupBox.style.transform = 'none'; // transform 속성 초기화 (중요)
}
});
}
// Mode 6 specific: Add connector line
if (POPUP_MODE === 6 &&x1 &&y1) {
const rect = this.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const targetX = mainRect.left + (mainRect.width * (x1 / 100));
const targetY = mainRect.top + (mainRect.height * (y1 / 100));
const isLeft = this.closest('.product-side').classList.contains('left');
const startX = isLeft ? rect.right : rect.left;
const startY = rect.top + (rect.height / 2);
const dx = targetX - startX;
const dy = targetY - startY;
const dist = Math.sqrt(dx * dx + dy * dy);
let angle = Math.atan2(dy, dx) * 180 / Math.PI;
if (!isLeft) angle += 180;
this.style.setProperty('--line-width', dist + 'px');
this.style.setProperty('--line-angle', angle + 'deg');
this.classList.add('show-connector');
requestAnimationFrame(() => this.classList.add('active'));
// Note: imgElement.style.transform = 'scale(1.2)' is not applied here
// as the image is either in the popup or not the primary focus for mode 6.
// If you want the side image to scale, uncomment the following:
// if (imgElement) imgElement.style.transform = 'scale(1.2)';
if (imgElement && proLink) {
// This click handler is for the side image, not the popup image
// imgElement.style.cursor = 'pointer'; // 이 줄을 주석 처리하거나 제거합니다.
imgElement.onclick = () => { location.href = proLink; };
}
}
} else if (POPUP_MODE === 4) {
// [모드 4] 1초 지연 후 중앙 팝업
popupTimer = setTimeout(() => {
if (imgElement) {
@@ -99,8 +357,8 @@
// [모드 2] 점선 연결선 정밀 계산
const rect = this.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const targetX = mainRect.left + (mainRect.width * (parseFloat(x1) / 100));
const targetY = mainRect.top + (mainRect.height * (parseFloat(y1) / 100));
const targetX = mainRect.left + (mainRect.width * (x1 / 100));
const targetY = mainRect.top + (mainRect.height * (y1 / 100));
const isLeft = this.closest('.product-side').classList.contains('left');
const startX = isLeft ? rect.right : rect.left;
const startY = rect.top + (rect.height / 2);
@@ -120,57 +378,15 @@
imgElement.style.cursor = 'pointer';
imgElement.onclick = () => { location.href = proLink; };
}
} else if (POPUP_MODE === 3 && imgElement) {
} else if (POPUP_MODE === 3 && imgElement) { // 3번 모드 로직 추가
// [모드 3] 제자리 600 확대
imgElement.src = imgSrc;
// imgElement.src = imgSrc;
imgElement.classList.add('mode-self-zoom');
if (proLink) {
imgElement.style.cursor = 'pointer';
imgElement.style.pointerEvents = 'auto';
imgElement.onclick = () => { location.href = proLink; };
}
} else if(POPUP_MODE === 5 && imgElement) {
// [모드 2] 점선 연결선 정밀 계산
const rect = this.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const targetX = mainRect.left + (mainRect.width * (parseFloat(x1) / 100));
const targetY = mainRect.top + (mainRect.height * (parseFloat(y1) / 100));
const isLeft = this.closest('.product-side').classList.contains('left');
const startX = isLeft ? rect.right : rect.left;
const startY = rect.top + (rect.height / 2);
const dx = targetX - startX;
const dy = targetY - startY;
const dist = Math.sqrt(dx * dx + dy * dy);
let angle = Math.atan2(dy, dx) * 180 / Math.PI;
if (!isLeft) angle += 180;
this.style.setProperty('--line-width', dist + 'px');
this.style.setProperty('--line-angle', angle + 'deg');
this.classList.add('show-connector');
requestAnimationFrame(() => this.classList.add('active'));
if (imgElement) imgElement.style.transform = 'scale(1.2)';
if (imgElement && proLink) {
imgElement.style.cursor = 'pointer';
imgElement.onclick = () => { location.href = proLink; };
}
var popupTimer = setTimeout(() => {
if (imgElement) {
const clone = imgElement.cloneNode(true);
clone.id = 'active-popup-img';
clone.src = imgSrc;
if (proLink) {
clone.style.cursor = 'pointer';
clone.style.pointerEvents = 'auto';
clone.onclick = () => { location.href = proLink; };
}
mainArea.appendChild(clone);
requestAnimationFrame(() => clone.classList.add('active'));
}
}, 1000); // 1000ms = 1초 대기
}
}
});
@@ -186,12 +402,58 @@
}
// 마우스가 아이템을 벗어나면 즉시 청소 (타이머 취소 포함)
item.addEventListener('mouseleave', clearAllPopups);
item.addEventListener('mouseleave', function() {
console.log('Mouse left item!');
console.log('Setting hoverTimeout to hide popup in', HOVER_DELAY, 'ms');
// 팝업 숨김을 HOVER_DELAY만큼 지연시킵니다.
hoverTimeout = setTimeout(() => {
console.log('hoverTimeout triggered: Hiding popup.');
clearAllPopupsContent(); // 내용만 초기화
const popupBox = document.getElementById('popup-box');
if (popupBox) {
gsap.killTweensOf(popupBox); // 현재 진행 중인 애니메이션을 즉시 중단
gsap.to(popupBox, {
width: 0,
height: 0,
opacity: 0,
duration: 0.4,
onComplete: () => {
popupBox.style.display = 'none';
}
});
}
}, HOVER_DELAY); // HOVER_DELAY만큼 지연
});
});
// 섹션 전체를 벗어나도 청소 (안전장치)
// 섹션 전체를 벗어나면 팝업 박스 숨기기
if (section) {
section.addEventListener('mouseleave', clearAllPopups);
section.addEventListener('mouseleave', function() {
console.log('Mouse left product-section!');
console.log('Setting hoverTimeout to hide popup in', HOVER_DELAY, 'ms');
// 팝업 숨김을 HOVER_DELAY만큼 지연시킵니다.
hoverTimeout = setTimeout(() => {
console.log('hoverTimeout triggered: Hiding popup.');
clearAllPopupsContent(); // 내용만 초기화
const popupBox = document.getElementById('popup-box');
if (popupBox) {
gsap.killTweensOf(popupBox); // 현재 진행 중인 애니메이션을 즉시 중단
gsap.to(popupBox, {
width: 0,
height: 0,
opacity: 0,
duration: 0.4,
onComplete: () => {
popupBox.style.display = 'none';
}
});
}
}, HOVER_DELAY); // HOVER_DELAY만큼 지연
});
}
}
@@ -201,4 +463,5 @@
} else {
initProductEvents();
}
})();
})();
@@ -1,64 +0,0 @@
function initProductEvents() {
// 💡 설정: 1(메인중앙), 2(점선), 3(제자리확대)
const POPUP_MODE = 1;
const productItems = document.querySelectorAll('.product-section .product-item');
const mainArea = document.querySelector('.product-main');
if (!productItems.length) return;
productItems.forEach(item => {
// 💡 [수정] 데이터 속성에서 이미지 경로 직접 읽기 (없으면 내부 img 사용)
const imgSrc = item.dataset.img || item.querySelector('.product-img img').src;
const imgElement = item.querySelector('.product-img img');
item.addEventListener('mouseenter', function() {
clearAllPopups();
const x1 = this.dataset.x1;
const y1 = this.dataset.y1;
if (x1 && y1) {
ProductModule.setHotspot(x1, y1);
if (POPUP_MODE === 1 && mainArea) {
// [모드 1] 메인 중앙 복제 팝업
const clone = document.createElement('img');
clone.src = imgSrc; // 💡 전달받은 경로로 로드
clone.id = 'active-popup-img';
mainArea.appendChild(clone);
requestAnimationFrame(() => clone.classList.add('active'));
} else if (POPUP_MODE === 2) {
// [모드 2] 점선 연결 (기존 로직 동일)
const rect = this.getBoundingClientRect();
const mainRect = mainArea.getBoundingClientRect();
const targetX = mainRect.left + (mainRect.width * (parseFloat(x1) / 100));
const targetY = mainRect.top + (mainRect.height * (parseFloat(y1) / 100));
const isLeft = this.closest('.product-side').classList.contains('left');
const startX = isLeft ? rect.right : rect.left;
const dx = targetX - startX;
const dy = targetY - (rect.top + rect.height/2);
let angle = Math.atan2(dy, dx) * 180 / Math.PI;
if (!isLeft) angle += 180;
this.style.setProperty('--line-width', Math.sqrt(dx*dx + dy*dy) + 'px');
this.style.setProperty('--line-angle', angle + 'deg');
this.classList.add('show-connector');
requestAnimationFrame(() => this.classList.add('active'));
} else if (POPUP_MODE === 3) {
// 💡 [모드 3] 제자리 400x400 확대
if (imgElement) {
imgElement.src = imgSrc; // 💡 이미지 경로 갱신
imgElement.classList.add('mode-self-zoom');
}
}
}
});
item.addEventListener('mouseleave', function() {
clearAllPopups();
if (imgElement) imgElement.classList.remove('mode-self-zoom');
});
});
}
@@ -1,99 +0,0 @@
function initProducts(productsData) {
const map = document.getElementById('main-map');
const hotspotLayer = document.getElementById('hotspot-layer');
const preview = document.getElementById('hover-preview');
const previewGrid = document.getElementById('preview-grid');
let isFixed = false;
const noImg = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMjIyIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZpbGw9IiM0NDQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmb250LWZhbWl5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjI0Ij5ObyBJbWFnZTwvdGV4dD48L3N2Zz4=";
// 1. 그룹화 로직
const groups = {};
productsData.forEach(p => {
const key = `${parseFloat(p.xPct).toFixed(1)}_${parseFloat(p.yPct).toFixed(1)}`;
if (!groups[key]) groups[key] = [];
groups[key].push(p);
});
// 2. 핫스팟 생성 및 이벤트
Object.keys(groups).forEach(key => {
const group = groups[key];
const hs = document.createElement('div');
hs.className = 'hotspot';
hs.style.left = group[0].xPct + '%';
hs.style.top = group[0].yPct + '%';
hs.style.transform = 'translate(-50%, -50%)';
const updateContent = (showFullContent) => {
const hasContent = group.some(p => p.content && p.content.trim() !== "");
const isSingle = group.length === 1;
if (showFullContent && hasContent) {
// 클릭 시: 상세 HTML 모드
previewGrid.className = `products-grid full-html-mode ${isSingle ? 'single-item' : 'multi-items'}`;
previewGrid.innerHTML = group.map(p => {
if (p.content && p.content.trim() !== "") {
return `<div class="editor-content-view">${p.content}</div>`;
} else {
const linkAttr = (p.link) ? `onclick="location.href='${p.link}'" style="cursor:pointer;"` : "";
return `<div class="product-item" ${linkAttr}>
<div class="product-img-box"><img src="${p.img_url}" onerror="this.onerror=null; this.src='${noImg}';"></div>
<div class="info-content">
<div class="text-[12px] text-blue-400 font-bold mb-2">${p.page}</div>
<h4 class="text-white text-[18px] font-bold mb-3">${p.title}</h4>
<p class="text-slate-400 text-[14px] leading-relaxed">${p.desc}</p>
${p.link ? '<div class="text-[11px] text-blue-500 mt-2">자세히 보기 ></div>' : ''}
</div>
</div>`;
}
}).join('<hr class="content-divider">');
} else {
// 호버 시: 요약 모드
const gridClass = isSingle ? 'single-item' : 'multi-items';
previewGrid.className = `products-grid ${gridClass}`;
previewGrid.innerHTML = group.map(p => {
const linkAttr = (p.link) ? `onclick="location.href='${p.link}'" style="cursor:pointer;"` : "";
return `<div class="product-item" ${linkAttr}>
<div class="product-img-box"><img src="${p.img_url}" onerror="this.onerror=null; this.src='${noImg}';"></div>
<div class="info-content">
<div class="text-[12px] text-blue-400 font-bold mb-2">${p.page}</div>
<h4 class="text-white text-[18px] font-bold mb-3">${p.title}</h4>
<p class="text-slate-400 text-[14px] leading-relaxed">${p.desc}</p>
</div>
</div>`;
}).join('');
}
};
hs.onmouseenter = () => {
if (isFixed) return;
updateContent(false);
preview.style.display = 'block';
preview.classList.remove('fixed-mode');
};
hs.onmouseleave = () => {
if (isFixed) return;
preview.style.display = 'none';
};
hs.onclick = (e) => {
e.stopPropagation();
isFixed = true;
updateContent(true);
preview.classList.add('fixed-mode');
preview.style.display = 'flex';
};
hotspotLayer.appendChild(hs);
});
// 외부 클릭 시 닫기
document.addEventListener('click', (e) => {
if (!preview.contains(e.target)) {
isFixed = false;
preview.style.display = 'none';
preview.classList.remove('fixed-mode');
}
});
}