// ======================================== // 메인 페이지 전용 JavaScript // ======================================== document.addEventListener('DOMContentLoaded', function() { // initRevealAnimations is now in common.js initCounterAnimation(); initVideoTabs(); initVideoLazyLoading(); initParallaxImages(); initHeroScrollFade(); initWorkflowScroll(); initPortfolioTabs(); }); // ======================================== // 통합 Reveal 애니메이션 시스템 // ======================================== function initRevealAnimations() { // prefers-reduced-motion 체크 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { // 모션 감소 모드: 즉시 표시 document.querySelectorAll('.reveal').forEach(el => { el.classList.add('revealed'); }); return; } const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const el = entry.target; const delay = parseInt(el.dataset.delay || '0', 10); setTimeout(() => { el.classList.add('revealed'); }, delay); observer.unobserve(el); } }); }, { threshold: 0.15, rootMargin: '0px 0px -60px 0px' }); document.querySelectorAll('.reveal').forEach(el => { observer.observe(el); }); } // ======================================== // 카운터 애니메이션 // ======================================== function initCounterAnimation() { const counters = document.querySelectorAll('.counter'); if (counters.length === 0) return; const animateCounter = (counter) => { const target = parseInt(counter.getAttribute('data-target')); const duration = 2000; const increment = target / (duration / 16); let current = 0; const updateCounter = () => { current += increment; if (current < target) { counter.textContent = Math.floor(current); requestAnimationFrame(updateCounter); } else { counter.textContent = target; } }; updateCounter(); }; const counterObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animateCounter(entry.target); counterObserver.unobserve(entry.target); } }); }, { threshold: 0.5 }); counters.forEach(counter => { counterObserver.observe(counter); }); } // ======================================== // 패럴랙스 효과 (이미지 시차 스크롤) // ======================================== function initParallaxImages() { const parallaxContainers = document.querySelectorAll('.preview-main, .preview-item'); if (parallaxContainers.length === 0) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; let ticking = false; window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(() => { parallaxContainers.forEach(container => { const rect = container.getBoundingClientRect(); const viewH = window.innerHeight; if (rect.bottom < 0 || rect.top > viewH) return; const progress = (viewH - rect.top) / (viewH + rect.height); const img = container.querySelector('.preview-img'); if (img) { const shift = (progress - 0.5) * 30; img.style.transform = `translateY(${shift}px)`; } }); ticking = false; }); ticking = true; } }); } // ======================================== // 히어로 스크롤 페이드 효과 // ======================================== function initHeroScrollFade() { const hero = document.querySelector('.hero-content-centered'); const indicator = document.querySelector('.hero-scroll-indicator'); if (!hero) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; let ticking = false; window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(() => { const scrollY = window.pageYOffset; const opacity = Math.max(0, 1 - scrollY / 500); const translate = scrollY * 0.3; hero.style.opacity = opacity; hero.style.transform = `translateY(${translate}px)`; if (indicator) indicator.style.opacity = Math.max(0, 1 - scrollY / 200); ticking = false; }); ticking = true; } }); } // ======================================== // 비디오 탭 시스템 // ======================================== function initVideoTabs() { const tabButtons = document.querySelectorAll('.video-tab-btn'); const tabContents = document.querySelectorAll('.video-tab-content'); if (tabButtons.length === 0) return; tabButtons.forEach(button => { button.addEventListener('click', () => { const targetTab = button.getAttribute('data-tab'); tabButtons.forEach(btn => btn.classList.remove('active')); button.classList.add('active'); tabContents.forEach(content => { content.classList.remove('active'); }); const activeContent = document.getElementById(targetTab); if (activeContent) { activeContent.classList.add('active'); activeContent.style.opacity = '0'; activeContent.style.transform = 'translateY(20px)'; setTimeout(() => { activeContent.style.transition = 'all 0.3s ease'; activeContent.style.opacity = '1'; activeContent.style.transform = 'translateY(0)'; }, 50); } }); button.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); button.click(); } }); }); } // ======================================== // 비디오 레이지 로딩 // ======================================== function initVideoLazyLoading() { const videoWrappers = document.querySelectorAll('.video-wrapper'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadVideo(entry.target); observer.unobserve(entry.target); } }); }, { threshold: 0.1, rootMargin: '50px 0px' }); videoWrappers.forEach(wrapper => { observer.observe(wrapper); if (!wrapper.querySelector('.video-loading')) { const loadingDiv = document.createElement('div'); loadingDiv.className = 'video-loading'; loadingDiv.textContent = '비디오 로딩 중...'; loadingDiv.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgba(255, 255, 255, 0.4); font-size: var(--font-sm); z-index: 10; `; wrapper.appendChild(loadingDiv); } }); } function loadVideo(wrapper) { const iframe = wrapper.querySelector('iframe'); if (iframe && iframe.dataset.src) { iframe.src = iframe.dataset.src; iframe.addEventListener('load', function() { wrapper.classList.add('loaded'); const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.remove(); } }); iframe.addEventListener('error', function() { const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.textContent = '비디오를 로드할 수 없습니다'; loadingDiv.style.color = '#ef4444'; } }); } else { wrapper.classList.add('loaded'); const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.remove(); } } } // ======================================== // 통합 스티키 쇼케이스 스크롤 // ======================================== function initWorkflowScroll() { const steps = document.querySelectorAll('.showcase-step'); const medias = document.querySelectorAll('.showcase-media'); const dots = document.querySelectorAll('.showcase-dot'); if (steps.length === 0) return; // 이미지 페이드 루프 타이머 관리 const fadeTimers = {}; function startFadeLoop(media) { const loop = media.querySelector('.showcase-fade-loop'); if (!loop) return; const imgs = loop.querySelectorAll('.fade-loop-img'); if (imgs.length <= 1) return; const interval = parseInt(loop.dataset.interval || '3000', 10); const key = media.dataset.step; if (fadeTimers[key]) return; // 이미 실행 중 let current = 0; imgs.forEach((img, i) => img.classList.toggle('active', i === 0)); fadeTimers[key] = setInterval(() => { imgs[current].classList.remove('active'); current = (current + 1) % imgs.length; imgs[current].classList.add('active'); }, interval); } function stopFadeLoop(media) { const key = media.dataset.step; if (fadeTimers[key]) { clearInterval(fadeTimers[key]); delete fadeTimers[key]; } } const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const stepNum = entry.target.dataset.step; steps.forEach(s => s.classList.remove('active')); medias.forEach(m => { m.classList.remove('active'); stopFadeLoop(m); }); dots.forEach(d => d.classList.remove('active')); entry.target.classList.add('active'); const activeMedia = document.querySelector(`.showcase-media[data-step="${stepNum}"]`); const activeDot = document.querySelector(`.showcase-dot[data-step="${stepNum}"]`); if (activeMedia) { activeMedia.classList.add('active'); startFadeLoop(activeMedia); } if (activeDot) activeDot.classList.add('active'); } }); }, { threshold: 0.4, rootMargin: '-10% 0px -10% 0px' }); steps.forEach(step => observer.observe(step)); // 첫 번째 스텝의 페이드 루프 시작 (해당되는 경우) const firstMedia = document.querySelector('.showcase-media.active'); if (firstMedia) startFadeLoop(firstMedia); } // ======================================== // 포트폴리오 탭 시스템 // ======================================== function initPortfolioTabs() { const tabBtns = document.querySelectorAll('.portfolio-tab-btn'); const tabPanels = document.querySelectorAll('.portfolio-tab-panel'); if (tabBtns.length === 0) return; tabBtns.forEach(btn => { btn.addEventListener('click', () => { const target = btn.dataset.portfolioTab; tabBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); tabPanels.forEach(panel => panel.classList.remove('active')); const activePanel = document.getElementById('portfolio-' + target); if (activePanel) { activePanel.classList.add('active'); // 탭 전환 시 lazy loading 트리거 activePanel.querySelectorAll('iframe[data-src]').forEach(iframe => { if (!iframe.src) { iframe.src = iframe.dataset.src; } }); } }); }); } // ======================================== // 마우스 호버 효과 (글래스 카드) // ======================================== document.querySelectorAll('.feature-card, .service-item').forEach(card => { card.addEventListener('mouseenter', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.style.setProperty('--mouse-x', `${x}px`); this.style.setProperty('--mouse-y', `${y}px`); }); card.addEventListener('mousemove', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.style.setProperty('--mouse-x', `${x}px`); this.style.setProperty('--mouse-y', `${y}px`); }); });