Files
kngil_home/kngil/js/results.js

350 lines
8.6 KiB
JavaScript

/**
* Results Page Controller
* 탭 전환 및 리포트 페이지 애니메이션
*
* @version 2.1.0 (Simplified)
*/
(function() {
'use strict';
// ============================================
// 설정
// ============================================
const config = {
// 선택자
selectors: {
wrap: '.results-wrap',
tabs: '.tab-list li a',
panels: '.tab-content',
pages: '.report-page'
},
// 탭 ID
tabIds: ['key-natural', 'key-social', 'key-cost'],
// 애니메이션 설정
animation: {
delay: 100, // 각 페이지 간격 (ms)
duration: 600, // 애니메이션 지속 (ms)
distance: 120 // 시작 위치 (px)
},
// 스크롤 탭 전환
scrollTab: {
enabled: true,
distance: '150%' // 핀 유지 거리
}
};
// ============================================
// 유틸리티
// ============================================
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => ctx.querySelectorAll(sel);
const hasGSAP = () => typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined';
// Debounce
const debounce = (fn, ms) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
};
// Throttle
const throttle = (fn, ms) => {
let waiting = false;
return (...args) => {
if (!waiting) {
fn(...args);
waiting = true;
setTimeout(() => waiting = false, ms);
}
};
};
// ============================================
// 탭 컨트롤러
// ============================================
const TabController = {
wrap: null,
tabs: null,
panels: null,
init() {
this.wrap = $(config.selectors.wrap);
if (!this.wrap) return;
this.tabs = $$(config.selectors.tabs);
this.panels = $$(config.selectors.panels);
this.setupEvents();
},
setupEvents() {
this.tabs.forEach((tab, i) => {
// 클릭
tab.addEventListener('click', (e) => {
e.preventDefault();
this.switch(i);
});
// 키보드
tab.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.switch(i);
}
});
});
},
switch(index) {
const targetId = config.tabIds[index];
const target = document.getElementById(targetId);
if (!target) return;
// 모든 패널 숨김
this.panels.forEach(panel => {
if (panel !== target) {
panel.classList.remove('on');
Animation.reset(panel);
}
});
// 선택된 패널 표시
target.classList.add('on');
// 탭 상태 업데이트
this.tabs.forEach((tab, i) => {
const li = tab.closest('li');
li.classList.toggle('on', i === index);
tab.setAttribute('aria-selected', i === index);
});
// 애니메이션 실행
setTimeout(() => Animation.run(target), 50);
// 스크롤 탭 인덱스 동기화
if (ScrollTab.enabled) {
ScrollTab.currentIndex = index;
}
}
};
// ============================================
// 애니메이션
// ============================================
const Animation = {
animated: new Set(),
init() {
if (!hasGSAP()) return;
const panels = $$(config.selectors.panels);
// 각 패널에 스크롤 트리거 설정
panels.forEach(panel => {
ScrollTrigger.create({
trigger: panel,
start: 'top center',
onEnter: () => this.onEnter(panel),
onEnterBack: () => this.onEnter(panel)
});
});
// 초기 상태 확인
this.checkInitial();
},
onEnter(panel) {
if (!panel.classList.contains('on')) return;
this.run(panel);
},
checkInitial() {
const active = $('.tab-content.on');
if (!active) return;
const rect = active.getBoundingClientRect();
if (rect.top <= window.innerHeight / 2) {
this.run(active);
}
},
run(panel) {
const pages = $$(config.selectors.pages, panel);
if (!pages.length) return;
const { delay, duration, distance } = config.animation;
if (hasGSAP()) {
// GSAP 애니메이션
gsap.set(pages, { opacity: 0, y: distance });
gsap.to(pages, {
opacity: 1,
y: 0,
duration: duration / 1000,
stagger: delay / 1000,
ease: 'power2.out'
});
} else {
// CSS 애니메이션
pages.forEach((page, i) => {
page.style.cssText = `
opacity: 0;
transform: translateY(${distance}px);
transition: none;
`;
requestAnimationFrame(() => {
setTimeout(() => {
page.style.cssText = `
opacity: 1;
transform: translateY(0);
transition: opacity ${duration}ms ease-out,
transform ${duration}ms ease-out;
`;
}, i * delay);
});
});
}
this.animated.add(panel.id);
},
reset(panel) {
const pages = $$(config.selectors.pages, panel);
if (!pages.length) return;
const { distance } = config.animation;
if (hasGSAP()) {
gsap.set(pages, { opacity: 0, y: distance });
} else {
pages.forEach(page => {
page.style.cssText = `
opacity: 0;
transform: translateY(${distance}px);
transition: none;
`;
});
}
}
};
// ============================================
// 스크롤 탭
// ============================================
const ScrollTab = {
enabled: false,
currentIndex: -1,
trigger: null,
init() {
if (!config.scrollTab.enabled || !hasGSAP()) return;
const wrap = $(config.selectors.wrap);
if (!wrap) return;
const tabCount = config.tabIds.length;
this.trigger = ScrollTrigger.create({
trigger: wrap,
start: 'top top',
end: `+=${config.scrollTab.distance}`,
pin: true,
invalidateOnRefresh: true,
onUpdate: throttle((self) => {
const index = Math.min(
Math.floor(self.progress * tabCount),
tabCount - 1
);
if (index !== this.currentIndex && index >= 0) {
this.currentIndex = index;
TabController.switch(index);
}
}, 50)
});
this.enabled = true;
}
};
// ============================================
// 리사이즈 핸들러
// ============================================
const ResizeHandler = {
init() {
window.addEventListener('resize', debounce(() => {
if (hasGSAP()) {
ScrollTrigger.refresh();
}
}, 250));
}
};
// ============================================
// 페이지 로드
// ============================================
const PageLoad = {
init() {
// 스크롤 복원 차단
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
// 최상단으로
if (window.scrollY > 0) {
window.scrollTo(0, 0);
}
// 로딩 완료
window.addEventListener('load', () => {
document.body.classList.add('loaded');
if (hasGSAP()) {
ScrollTrigger.refresh();
}
});
}
};
// ============================================
// 초기화
// ============================================
const init = () => {
// 즉시 실행
PageLoad.init();
// DOM 준비 후
const start = () => {
if (hasGSAP()) {
gsap.registerPlugin(ScrollTrigger);
}
TabController.init();
Animation.init();
ScrollTab.init();
ResizeHandler.init();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
};
// ============================================
// 전역 API
// ============================================
window.ResultsPageController = {
switchTab: (i) => TabController.switch(i),
refresh: () => hasGSAP() && ScrollTrigger.refresh(),
config: config
};
// 시작
init();
})();