/** * Provided Page Controller * 스크롤 기반 애니메이션 및 네비게이션 제어 */ (function() { 'use strict'; // ============================================ // Configuration // ============================================ const CONFIG = { SELECTORS: { fixLeftTit: '.js-fixLeft-tit', fixLeftTitItems: '.js-fixLeft-tit > li', fixLeftBg: '.js-fixLeft-bg', fixLeftSecs: '.js-fixLeft-secs', route: '.route' }, ANIMATION: { bgScale: 1, titScale: 0.7, titTranslate: '-47%', duration: 0.5 } }; // ============================================ // Utility Functions // ============================================ const Utils = { $(selector) { return document.querySelector(selector); }, $$(selector) { return document.querySelectorAll(selector); } }; // ============================================ // Smooth Scroll Function // ============================================ window.goto = function(id) { const el = document.getElementById(id); if (el) { el.scrollIntoView({ behavior: 'smooth' }); } }; // ============================================ // FixLeft Controller (Scroll-based Animation) // ============================================ const FixLeftController = { titElements: null, bgElements: null, sections: null, init() { const titRoot = Utils.$(CONFIG.SELECTORS.fixLeftTit); if (!titRoot) { RouteController.init(); return; } // GSAP 및 ScrollTrigger 확인 if (typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') { console.warn('[FixLeftController] GSAP or ScrollTrigger not loaded'); RouteController.init(); return; } gsap.registerPlugin(ScrollTrigger); this.titElements = Utils.$$(CONFIG.SELECTORS.fixLeftTitItems); this.bgElements = Utils.$$(CONFIG.SELECTORS.fixLeftBg); this.sections = Utils.$$(`${CONFIG.SELECTORS.fixLeftSecs} > div, ${CONFIG.SELECTORS.fixLeftSecs} > section`); this.setupScrollTriggers(); this.setupClickHandlers(); }, setupScrollTriggers() { this.sections.forEach((section, index) => { if (!section) return; ScrollTrigger.create({ trigger: section, start: 'top center', onEnter: () => this.updateElements(index), onLeaveBack: () => this.updateElements(index) }); }); }, updateElements(activeIndex) { // 배경 애니메이션 this.bgElements.forEach((bg, index) => { const isActive = index === activeIndex; bg.classList.toggle('on', isActive); this.setBgActive(bg, isActive); }); // 타이틀 애니메이션 및 접근성 this.titElements.forEach((tit, index) => { const isActive = index === activeIndex; tit.classList.toggle('on', isActive); tit.setAttribute('aria-selected', isActive ? 'true' : 'false'); tit.setAttribute('tabindex', isActive ? '0' : '-1'); this.setTitActive(tit, isActive); }); }, setupClickHandlers() { if (!this.titElements.length || !this.sections.length) return; this.titElements.forEach((title, index) => { title.addEventListener('click', () => { this.scrollToSection(index); }); title.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.scrollToSection(index); } }); }); }, scrollToSection(index) { const section = this.sections[index]; if (!section) return; section.scrollIntoView({ behavior: 'smooth', block: 'start' }); this.updateElements(index); }, setBgActive(element, active) { gsap.to(element, { transform: active ? `scale(${CONFIG.ANIMATION.bgScale})` : 'scale(1)', duration: CONFIG.ANIMATION.duration }); }, setTitActive(element, active) { gsap.to(element, { opacity: active ? 1 : 0.5, transform: active ? 'scale(1) translate(0%, 0%)' : `scale(${CONFIG.ANIMATION.titScale}) translate(${CONFIG.ANIMATION.titTranslate}, 0%)`, duration: CONFIG.ANIMATION.duration }); } }; // ============================================ // Route Controller (Intersection Observer) // ============================================ const RouteController = { routeElement: null, sections: null, tabs: null, subs: null, imgs: null, observer: null, init() { this.routeElement = Utils.$(CONFIG.SELECTORS.route); if (!this.routeElement) return; this.sections = this.routeElement.querySelectorAll('#sec1, #sec2, #sec3'); this.tabs = this.routeElement.querySelectorAll('.tabs .tabs-li'); this.subs = this.routeElement.querySelectorAll('.subs li'); this.imgs = this.routeElement.querySelectorAll('.imgs li'); if (this.sections.length === 0) return; this.setupObserver(); }, setupObserver() { this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { root: null, rootMargin: '0px', threshold: 0.5 } ); this.sections.forEach(section => { this.observer.observe(section); }); }, handleIntersection(entries) { entries .filter(entry => entry.isIntersecting) .forEach(entry => { const id = entry.target.id; const index = id ? parseInt(id.replace('sec', ''), 10) - 1 : -1; if (index < 0) return; // 모든 그룹에 동일한 인덱스 적용 [this.tabs, this.subs, this.imgs].forEach(group => { group.forEach((el, i) => { el.classList.toggle('on', i === index); }); }); }); } }; // ============================================ // Initialization // ============================================ const init = () => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { FixLeftController.init(); }); } else { FixLeftController.init(); } }; init(); })(); /** * Data Provision - Responsive Offset Path * .data-bullet 요소의 offset-path를 화면 크기에 맞춰 동적으로 업데이트 */ (function() { 'use strict'; // 반응형 offset-path 업데이트 함수 function updateOffsetPath() { const container = document.querySelector('.provided .data-provision'); const bullets = document.querySelectorAll('.data-bullet'); if (!container || bullets.length === 0) return; // 컨테이너의 실제 너비와 높이 가져오기 const containerWidth = container.offsetWidth; const containerHeight = container.offsetHeight; // 원본 비율 (720 x 270) const originalWidth = 720; const originalHeight = 270; const originalRadius = 135; // 실제 크기에 맞춰 계산 const radius = (containerWidth / originalWidth) * originalRadius; const width = containerWidth; const height = containerHeight; // SVG path 생성 (둥근 사각형 형태) const pathData = `M ${radius},0 L ${width - radius},0 A ${radius} ${radius} 0 0 1 ${width} ${radius} A ${radius} ${radius} 0 0 1 ${width - radius} ${height} L ${radius},${height} A ${radius} ${radius} 0 0 1 0 ${radius} A ${radius} ${radius} 0 0 1 ${radius} 0 Z`; // 모든 bullet 요소에 적용 bullets.forEach(bullet => { bullet.style.offsetPath = `path("${pathData}")`; }); } // ResizeObserver를 사용한 반응형 처리 function initResponsiveOffsetPath() { const container = document.querySelector('.provided .data-provision'); if (!container) { console.warn('Data provision container not found'); return; } // ResizeObserver 생성 (성능 최적화) const resizeObserver = new ResizeObserver(entries => { // requestAnimationFrame으로 성능 최적화 requestAnimationFrame(() => { updateOffsetPath(); }); }); // 컨테이너 관찰 시작 resizeObserver.observe(container); // 초기 실행 updateOffsetPath(); } // DOM 로드 완료 후 초기화 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initResponsiveOffsetPath); } else { initResponsiveOffsetPath(); } // 폰트 로드 완료 후 재계산 (레이아웃 변경 가능성) if (document.fonts && document.fonts.ready) { document.fonts.ready.then(() => { setTimeout(updateOffsetPath, 100); }); } })();