// ======================================== // 공통 JavaScript 모듈 // ======================================== // 다크모드 FOUC 방지: DOM 로드 전에 즉시 실행 (function() { const saved = localStorage.getItem('theme'); if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.setAttribute('data-theme', 'dark'); } })(); // DOM 로드 완료 후 실행 document.addEventListener('DOMContentLoaded', async function() { showPageLoading(); initializeNavigation(); setActiveNavLink(); await loadComponents(); initLazyLoading(); initRevealAnimations(); createFloatingSNS(); hidePageLoading(); // 컴포넌트 로드 후 스크롤 위치 보정 if (window.scrollY > 0 && !window.location.hash) { window.scrollTo(0, 0); } }); // ======================================== // 컴포넌트 로드 함수 // ======================================== async function loadComponents() { // Header 로드 const headerPlaceholder = document.getElementById('header-placeholder'); if (headerPlaceholder) { try { const response = await fetch('components/header.html'); const html = await response.text(); headerPlaceholder.innerHTML = html; initializeNavigation(); // 헤더 로드 후 네비게이션 초기화 initThemeToggle(); // 테마 토글 초기화 } catch (error) { console.error('Error loading header:', error); } } // Footer 로드 const footerPlaceholder = document.getElementById('footer-placeholder'); if (footerPlaceholder) { try { const response = await fetch('components/footer.html'); const html = await response.text(); footerPlaceholder.innerHTML = html; // 동적 푸터 로드 성공 시 백업 푸터 숨기기 const backupFooter = document.querySelector('footer.site-footer'); if (backupFooter) { backupFooter.style.display = 'none'; } } catch (error) { console.error('Error loading footer:', error); // 동적 푸터 로드 실패 시 백업 푸터 유지 } } } // ======================================== // 네비게이션 기능 // ======================================== function initializeNavigation() { const hamburger = document.querySelector('.hamburger'); const navMenu = document.querySelector('.nav-menu'); if (hamburger && navMenu) { hamburger.addEventListener('click', function() { const isActive = hamburger.classList.toggle('active'); navMenu.classList.toggle('active'); hamburger.setAttribute('aria-expanded', isActive); hamburger.setAttribute('aria-label', isActive ? '메뉴 닫기' : '메뉴 열기'); }); // 네비게이션 링크 클릭 시 모바일 메뉴 닫기 document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', () => { hamburger.classList.remove('active'); navMenu.classList.remove('active'); hamburger.setAttribute('aria-expanded', 'false'); hamburger.setAttribute('aria-label', '메뉴 열기'); }); }); } // 스크롤 시 네비게이션 바 스타일 변경 (RAF 최적화) let scrollTicking = false; const navbar = document.querySelector('.navbar'); window.addEventListener('scroll', () => { if (!scrollTicking) { requestAnimationFrame(() => { if (navbar) { navbar.classList.toggle('scrolled', window.pageYOffset > 20); } scrollTicking = false; }); scrollTicking = true; } }); } // ======================================== // 현재 페이지 네비게이션 링크 활성화 // ======================================== function setActiveNavLink() { const currentPath = window.location.pathname; const navLinks = document.querySelectorAll('.nav-link'); navLinks.forEach(link => { const linkPath = link.getAttribute('href'); if (currentPath === linkPath || (currentPath === '/' && linkPath === '/') || (currentPath.includes(linkPath) && linkPath !== '/')) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } // ======================================== // 스크롤 애니메이션 // ======================================== function initScrollAnimations() { const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -100px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate-in'); } }); }, observerOptions); // 애니메이션 대상 요소 관찰 document.querySelectorAll('.animate-on-scroll').forEach(el => { observer.observe(el); }); } // ======================================== // 유틸리티 함수 // ======================================== // 이메일 유효성 검사 function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } // 알림 표시 function showNotification(message, type = 'info') { // 기존 알림 제거 const existingNotification = document.querySelector('.notification'); if (existingNotification) { existingNotification.remove(); } // 새 알림 생성 const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; // 스타일 적용 notification.style.cssText = ` position: fixed; top: 80px; right: 20px; padding: 1rem 2rem; border-radius: 10px; color: white; font-weight: 600; z-index: 10000; transform: translateX(400px); transition: transform 0.3s ease; `; // 타입별 배경색 const bgColors = { success: '#10b981', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' }; notification.style.backgroundColor = bgColors[type] || bgColors.info; document.body.appendChild(notification); // 애니메이션 setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); // 자동 제거 setTimeout(() => { notification.style.transform = 'translateX(400px)'; setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 300); }, 3000); } // 디바운스 함수 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 스로틀 함수 function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // ======================================== // Lazy Loading for YouTube iframes // ======================================== function initLazyLoading() { // YouTube iframe lazy loading const lazyIframes = document.querySelectorAll('iframe[data-src]'); if ('IntersectionObserver' in window) { const iframeObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const iframe = entry.target; iframe.src = iframe.dataset.src; iframe.removeAttribute('data-src'); iframeObserver.unobserve(iframe); } }); }, { rootMargin: '50px' }); lazyIframes.forEach(iframe => { iframeObserver.observe(iframe); }); } else { // Fallback for older browsers lazyIframes.forEach(iframe => { iframe.src = iframe.dataset.src; iframe.removeAttribute('data-src'); }); } // Image lazy loading (if any) const lazyImages = document.querySelectorAll('img[data-src]'); if ('IntersectionObserver' in window && lazyImages.length > 0) { const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); imageObserver.unobserve(img); } }); }, { rootMargin: '50px' }); lazyImages.forEach(img => { imageObserver.observe(img); }); } } // ======================================== // Export 함수들 (다른 스크립트에서 사용 가능) // ======================================== // ======================================== // 로딩 상태 관리 // ======================================== function showPageLoading() { // 페이지 로딩 오버레이가 이미 있는지 확인 if (document.getElementById('pageLoadingOverlay')) return; const loadingOverlay = document.createElement('div'); loadingOverlay.id = 'pageLoadingOverlay'; loadingOverlay.className = 'loading-overlay'; loadingOverlay.innerHTML = `
페이지를 불러오는 중...
`; document.body.appendChild(loadingOverlay); } function hidePageLoading() { const loadingOverlay = document.getElementById('pageLoadingOverlay'); if (loadingOverlay) { setTimeout(() => { loadingOverlay.style.opacity = '0'; setTimeout(() => { if (loadingOverlay.parentNode) { loadingOverlay.remove(); } }, 150); }, 100); // 최소 0.1초 표시 } } function showComponentLoading(element, text = '로딩 중...') { element.innerHTML = `
${text}
`; } function hideComponentLoading(element, content) { element.innerHTML = content; } // ======================================== // 플로팅 SNS 버튼 생성 // ======================================== function createFloatingSNS() { // 이미 존재하면 생성하지 않음 if (document.querySelector('.floating-sns')) return; const snsContainer = document.createElement('div'); snsContainer.className = 'floating-sns'; snsContainer.innerHTML = ` `; document.body.appendChild(snsContainer); } // ======================================== // 다크모드 토글 // ======================================== function initThemeToggle() { const toggle = document.getElementById('themeToggle'); if (!toggle) return; toggle.addEventListener('click', function() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const newTheme = isDark ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); toggle.setAttribute('aria-label', newTheme === 'dark' ? '라이트 모드로 전환' : '다크 모드로 전환' ); }); // 초기 aria-label 설정 const current = document.documentElement.getAttribute('data-theme'); toggle.setAttribute('aria-label', current === 'dark' ? '라이트 모드로 전환' : '다크 모드로 전환' ); } // ======================================== // 공통 Reveal 애니메이션 // ======================================== function initRevealAnimations() { 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); }); } // ======================================== // Export 함수들 (다른 스크립트에서 사용 가능) // ======================================== window.commonUtils = { showNotification, isValidEmail, debounce, throttle, initScrollAnimations, initLazyLoading, showPageLoading, hidePageLoading, showComponentLoading, hideComponentLoading };