mingle-website/js/common.js
68893236+KINDNICK@users.noreply.github.com 491a6b40b2 Add: 플로팅 CTA 버튼 + 메인페이지 문의폼 임베드 + CTA 카카오톡 카드
- 플로팅 문의 버튼: 전 페이지 하단 좌측 고정 (카카오톡/전화/문의 3옵션)
- 메인페이지 CTA 섹션: 오시는 길 → 카카오톡 오픈채팅 카드로 교체
- 메인페이지 하단: 온라인 문의폼 직접 임베드 (contact 페이지 이동 불필요)
- FAQ 바로가기: 온라인 문의하기 버튼 추가 (#inline-contact 앵커)
- i18n 4개 언어 번역 키 추가 (floating CTA, CTA 카카오톡, 문의폼)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:58:15 +09:00

510 lines
18 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();
// i18n 초기화 (언어 감지 + JSON 로드)
if (window.i18n) {
await window.i18n.init();
}
initializeNavigation();
setActiveNavLink();
await loadComponents();
// 컴포넌트 로드 후 i18n 번역 적용 + 언어 스위처 초기화
if (window.i18n) {
window.i18n.translateDOM();
window.i18n.initSwitcher();
}
initLazyLoading();
initRevealAnimations();
createFloatingSNS();
createFloatingCTA();
hidePageLoading();
// 컴포넌트 로드 후 스크롤 위치 보정
if (window.scrollY > 0 && !window.location.hash) {
window.scrollTo(0, 0);
}
});
// ========================================
// 컴포넌트 로드 함수
// ========================================
async function loadComponents() {
// Header 로드
const headerPlaceholder = document.getElementById('header-placeholder');
if (headerPlaceholder) {
if (!headerPlaceholder.innerHTML.trim()) {
try {
const response = await fetch('/components/header.html');
const html = await response.text();
headerPlaceholder.innerHTML = html;
} catch (error) {
console.error('Error loading header:', error);
}
}
initializeNavigation(); // 헤더 로드 후 네비게이션 초기화
initThemeToggle(); // 테마 토글 초기화
}
// Footer 로드
const footerPlaceholder = document.getElementById('footer-placeholder');
if (footerPlaceholder) {
if (!footerPlaceholder.innerHTML.trim()) {
try {
const response = await fetch('/components/footer.html');
const html = await response.text();
footerPlaceholder.innerHTML = html;
// 동적 푸터 로드 성공 시 백업 푸터 숨기기
const backupFooter = document.getElementById('backupFooter');
if (backupFooter) {
backupFooter.style.display = 'none';
}
} catch (error) {
console.error('Error loading footer:', error);
// 동적 푸터 로드 실패 시 백업 푸터 유지
}
} else {
const backupFooter = document.getElementById('backupFooter');
if (backupFooter) {
backupFooter.style.display = 'none';
}
}
}
}
// ========================================
// 네비게이션 기능
// ========================================
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);
const closeLabel = (window.i18n && window.i18n.t) ? window.i18n.t('header.menuClose', '메뉴 닫기') : '메뉴 닫기';
const openLabel = (window.i18n && window.i18n.t) ? window.i18n.t('header.menuOpen', '메뉴 열기') : '메뉴 열기';
hamburger.setAttribute('aria-label', isActive ? closeLabel : openLabel);
});
// 네비게이션 링크 클릭 시 모바일 메뉴 닫기
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', (window.i18n && window.i18n.t) ? window.i18n.t('header.menuOpen', '메뉴 열기') : '메뉴 열기');
});
});
}
// 스크롤 시 네비게이션 바 스타일 변경 (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';
const loadingText = (window.i18n && window.i18n.t) ? window.i18n.t('common.loading', '페이지를 불러오는 중...') : '페이지를 불러오는 중...';
loadingOverlay.innerHTML = `
<div class="loading-spinner"></div>
<div class="loading-text">${loadingText}</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);
}
// ========================================
// 플로팅 CTA (문의하기) 버튼
// ========================================
function createFloatingCTA() {
if (document.querySelector('.floating-cta')) return;
// contact 페이지에서는 표시하지 않음
if (window.location.pathname.includes('contact')) return;
var _t = function(key, fallback) {
return (window.i18n && window.i18n.t) ? window.i18n.t(key, fallback) : fallback;
};
var cta = document.createElement('div');
cta.className = 'floating-cta';
cta.innerHTML =
'<div class="cta-options">' +
'<a href="https://open.kakao.com/o/sXOK0Iji" target="_blank" rel="noopener noreferrer" class="cta-option cta-kakao" title="' + _t('common.floatingKakao', '카카오톡 상담') + '">' +
'<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C6.477 3 2 6.463 2 10.691c0 2.724 1.8 5.113 4.508 6.457-.2.744-.723 2.694-.828 3.112-.13.517.19.51.399.371.164-.109 2.61-1.774 3.67-2.496.727.104 1.478.159 2.251.159 5.523 0 10-3.463 10-7.603C22 6.463 17.523 3 12 3z"/></svg>' +
'<span>' + _t('common.floatingKakao', '카카오톡 상담') + '</span>' +
'</a>' +
'<a href="tel:+82-10-9288-9190" class="cta-option cta-phone" title="' + _t('common.floatingPhone', '전화 문의') + '">' +
'<i class="fa-solid fa-phone"></i>' +
'<span>' + _t('common.floatingPhone', '전화 문의') + '</span>' +
'</a>' +
'<a href="/contact" class="cta-option cta-contact" title="' + _t('common.floatingContact', '문의 페이지') + '">' +
'<i class="fa-solid fa-envelope"></i>' +
'<span>' + _t('common.floatingContact', '문의 페이지') + '</span>' +
'</a>' +
'</div>' +
'<button class="cta-main-btn" aria-label="' + _t('common.floatingCTA', '문의하기') + '">' +
'<i class="fa-solid fa-headset cta-icon-open"></i>' +
'<i class="fa-solid fa-xmark cta-icon-close"></i>' +
'</button>';
document.body.appendChild(cta);
var mainBtn = cta.querySelector('.cta-main-btn');
mainBtn.addEventListener('click', function() {
cta.classList.toggle('active');
});
// 외부 클릭 시 닫기
document.addEventListener('click', function(e) {
if (!cta.contains(e.target)) {
cta.classList.remove('active');
}
});
}
// ========================================
// 다크모드 토글
// ========================================
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);
const lightLabel = (window.i18n && window.i18n.t) ? window.i18n.t('header.lightMode', '라이트 모드로 전환') : '라이트 모드로 전환';
const darkLabel = (window.i18n && window.i18n.t) ? window.i18n.t('header.darkMode', '다크 모드 전환') : '다크 모드 전환';
toggle.setAttribute('aria-label', newTheme === 'dark' ? lightLabel : darkLabel);
});
// 초기 aria-label 설정
const current = document.documentElement.getAttribute('data-theme');
const lightLabelInit = (window.i18n && window.i18n.t) ? window.i18n.t('header.lightMode', '라이트 모드로 전환') : '라이트 모드로 전환';
const darkLabelInit = (window.i18n && window.i18n.t) ? window.i18n.t('header.darkMode', '다크 모드 전환') : '다크 모드 전환';
toggle.setAttribute('aria-label', current === 'dark' ? lightLabelInit : darkLabelInit);
}
// ========================================
// 공통 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
};