mingle-website/js/common.js
68893236+KINDNICK@users.noreply.github.com fa237316df Update: 전체 디자인 개선, 접근성 강화, OG 이미지 변경
- 다크모드 지원 추가 (모든 페이지 CSS)
- 글래스모피즘 카드 스타일 통일
- 하드코딩 컬러 → CSS 변수 전환
- 접근성 개선: skip nav, aria-label, sr-only, role="dialog"
- 파일 구조 정리: 이미지/로고 images/ 폴더로 이동, 미사용 파일 삭제
- 서비스 패키지 8시간 → 6시간으로 변경
- OG 이미지를 전용 mingle-OG.png로 변경
- 컨택트 다크모드 위치 박스 투명 처리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 02:44:19 +09:00

439 lines
14 KiB
JavaScript

// ========================================
// 공통 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 = `
<div class="loading-spinner"></div>
<div class="loading-text">페이지를 불러오는 중...</div>
`;
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 = `
<div class="component-loading">
<div class="loading-spinner"></div>
<div class="loading-text">${text}</div>
</div>
`;
}
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 = `
<a href="https://www.youtube.com/@minglestudio_mocap" target="_blank" rel="noopener noreferrer" class="sns-btn sns-btn-youtube" title="YouTube">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</a>
<a href="https://x.com/minglestudio" target="_blank" rel="noopener noreferrer" class="sns-btn sns-btn-x" title="X (Twitter)">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
`;
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
};