- 다크모드 지원 추가 (모든 페이지 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>
387 lines
13 KiB
JavaScript
387 lines
13 KiB
JavaScript
// ========================================
|
|
// 메인 페이지 전용 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`);
|
|
});
|
|
});
|