Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,705 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function initLottieIcons() {
|
||||
function loadLottieScript() {
|
||||
if (typeof window.lottie !== 'undefined') return Promise.resolve(true);
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
const existing = document.querySelector('script[data-lottie-loader="true"]');
|
||||
if (existing) {
|
||||
existing.addEventListener('load', function () { resolve(typeof window.lottie !== 'undefined'); });
|
||||
existing.addEventListener('error', function () { resolve(false); });
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = '/edu/js/lib/lottie.min.js';
|
||||
script.async = true;
|
||||
script.dataset.lottieLoader = 'true';
|
||||
script.addEventListener('load', function () { resolve(typeof window.lottie !== 'undefined'); });
|
||||
script.addEventListener('error', function () { resolve(false); });
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const icons = document.querySelectorAll('.lottie-icon[data-lottie-url]');
|
||||
|
||||
function initOne(el) {
|
||||
const url = el.getAttribute('data-lottie-url');
|
||||
if (!url || el.dataset.lottieInited === 'true') return;
|
||||
|
||||
const fallbackImg = el.parentElement ? el.parentElement.querySelector('.lottie-fallback') : null;
|
||||
el.dataset.lottieInited = 'true';
|
||||
|
||||
try {
|
||||
const anim = window.lottie.loadAnimation({
|
||||
container: el,
|
||||
renderer: 'svg',
|
||||
loop: !reduceMotion,
|
||||
autoplay: !reduceMotion,
|
||||
path: url,
|
||||
rendererSettings: { progressiveLoad: true },
|
||||
});
|
||||
|
||||
if (fallbackImg) fallbackImg.style.display = 'none';
|
||||
|
||||
anim.addEventListener('data_failed', function () {
|
||||
el.dataset.lottieInited = 'false';
|
||||
if (fallbackImg) fallbackImg.style.display = '';
|
||||
});
|
||||
} catch (_error) {
|
||||
el.dataset.lottieInited = 'false';
|
||||
if (fallbackImg) fallbackImg.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
loadLottieScript().then(function (ok) {
|
||||
if (!ok || typeof window.lottie === 'undefined') return;
|
||||
icons.forEach(initOne);
|
||||
});
|
||||
}
|
||||
|
||||
function initRankToggle() {
|
||||
const button = document.querySelector('.btn-rank-toggle');
|
||||
const popup = document.getElementById('rankListPopup');
|
||||
|
||||
if (!button || !popup) return;
|
||||
|
||||
button.addEventListener('click', function () {
|
||||
const isOpen = button.classList.toggle('is-open');
|
||||
popup.hidden = !isOpen;
|
||||
button.setAttribute('aria-expanded', String(isOpen));
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!button.classList.contains('is-open')) return;
|
||||
if (button.contains(event.target) || popup.contains(event.target)) return;
|
||||
button.classList.remove('is-open');
|
||||
popup.hidden = true;
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function initButtons() {
|
||||
document.querySelectorAll('.btn-reset-goal, .btn-next-growth').forEach(function (button) {
|
||||
const nextUrl = button.getAttribute('data-next-url') || button.getAttribute('data-reset-url');
|
||||
if (!nextUrl) return;
|
||||
|
||||
button.addEventListener('click', function () {
|
||||
window.location.href = nextUrl;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ensureSectionRevealVisible() {
|
||||
document.querySelectorAll('.scroll-section-reveal').forEach(function (section) {
|
||||
section.classList.add('is-visible');
|
||||
});
|
||||
}
|
||||
|
||||
function initVideoModal() {
|
||||
const VideoModalCtor =
|
||||
(typeof window !== 'undefined' && typeof window.VideoModalManager === 'function')
|
||||
? window.VideoModalManager
|
||||
: (typeof VideoModalManager === 'function' ? VideoModalManager : null);
|
||||
|
||||
if (typeof VideoModalCtor !== 'function' || !Array.isArray(window.MYCLASS_PAGE_VIDEOS)) {
|
||||
console.warn('[myclass_list] initVideoModal failed:', {
|
||||
VideoModalCtor: typeof VideoModalCtor,
|
||||
MYCLASS_PAGE_VIDEOS: window.MYCLASS_PAGE_VIDEOS?.length
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[myclass_list] MYCLASS_PAGE_VIDEOS loaded:', window.MYCLASS_PAGE_VIDEOS);
|
||||
|
||||
// 같은 학습 목표 영상 필터링을 위해 전체 영상 백업
|
||||
var allVideosBackup = (window.MYCLASS_PAGE_VIDEOS || []).slice();
|
||||
|
||||
const modalManager = new VideoModalCtor({ videos: window.MYCLASS_PAGE_VIDEOS });
|
||||
if (typeof modalManager.init === 'function') {
|
||||
modalManager.init();
|
||||
}
|
||||
|
||||
// --- 영상모달 닫힐 때 포스트잇 즉시 표시 (새로고침 없이) ---
|
||||
if (typeof modalManager === 'object' && typeof modalManager.close === 'function') {
|
||||
const origClose = modalManager.close.bind(modalManager);
|
||||
modalManager.close = async function(...args) {
|
||||
// 닫기 전, 시청 완료 여부 확인
|
||||
const video = this.currentVideo;
|
||||
const contentId = video && (video.content_id || video.id);
|
||||
const duration = video && (video.content_tm || video.duration || 0);
|
||||
const watchTm = video && (video.watch_tm || 0);
|
||||
// 90% 이상 시청 체크 (혹은 서버에서 completed_at 반환 시 활용)
|
||||
let isCompleted = false;
|
||||
if (duration > 0 && watchTm >= 0.9 * duration) {
|
||||
isCompleted = true;
|
||||
}
|
||||
// 서버에서 completed_at 반환값 활용 (권장)
|
||||
if (video && video.completed_at) {
|
||||
isCompleted = true;
|
||||
}
|
||||
// 닫기 후 DOM 갱신
|
||||
const result = await origClose(...args);
|
||||
if (isCompleted && contentId) {
|
||||
// 해당 카드 DOM 갱신
|
||||
const item = document.querySelector('.books-item[data-video-id="' + contentId + '"]');
|
||||
if (item && !item.classList.contains('books-item-completed')) {
|
||||
item.classList.add('books-item-completed');
|
||||
// 포스트잇이 없으면 추가 (서버 렌더 구조와 동일하게)
|
||||
if (!item.querySelector('.book-postit')) {
|
||||
const postit = document.createElement('div');
|
||||
postit.className = 'book-postit';
|
||||
postit.innerHTML = '<strong class="book-postit-title">CHECK!</strong><ul class="book-postit-list"><li>영상 시청 완료</li></ul>';
|
||||
const infoWrap = item.querySelector('.book-info-wrap');
|
||||
if (infoWrap) infoWrap.appendChild(postit);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
document.querySelectorAll('.books-list').forEach(function (list) {
|
||||
list.addEventListener('click', function (event) {
|
||||
const item = event.target.closest('.books-item');
|
||||
if (!item || event.target.closest('.bookmark')) return;
|
||||
|
||||
event.preventDefault();
|
||||
const id = String(item.getAttribute('data-video-id') || '').trim();
|
||||
console.log('[myclass_list] Video item clicked:', {
|
||||
id,
|
||||
dataVideoId: item.getAttribute('data-video-id'),
|
||||
videoData: window.MYCLASS_PAGE_VIDEOS.find(v => String(v.id) === String(id))
|
||||
});
|
||||
|
||||
if (!id) return;
|
||||
|
||||
// 같은 학습 목표 영상만 추천 리스트에 표시 (최대 6개)
|
||||
var clickedVid = null;
|
||||
for (var i = 0; i < allVideosBackup.length; i++) {
|
||||
if (String(allVideosBackup[i].id) === String(id) || String(allVideosBackup[i].content_id) === String(id)) {
|
||||
clickedVid = allVideosBackup[i]; break;
|
||||
}
|
||||
}
|
||||
if (clickedVid && clickedVid.goal_code) {
|
||||
var sameGoalVids = allVideosBackup.filter(function(v) { return v.goal_code === clickedVid.goal_code; });
|
||||
if (Array.isArray(modalManager.videos)) modalManager.videos = sameGoalVids;
|
||||
if (modalManager.config && Array.isArray(modalManager.config.videos)) modalManager.config.videos = sameGoalVids;
|
||||
}
|
||||
|
||||
if (typeof modalManager.openVideo === 'function') {
|
||||
console.log('[myclass_list] Calling openVideo with:', id);
|
||||
modalManager.openVideo(id);
|
||||
} else if (typeof modalManager.loadVideoModal === 'function') {
|
||||
console.log('[myclass_list] Calling loadVideoModal with:', id);
|
||||
modalManager.loadVideoModal(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initGsapScrollEvents() {
|
||||
if (typeof window.gsap === 'undefined' || typeof window.ScrollTrigger === 'undefined') return false;
|
||||
|
||||
const main = document.querySelector('.myclass-main');
|
||||
const content = document.querySelector('.myclass-content');
|
||||
const sections = window.gsap.utils.toArray('.scroll-section');
|
||||
const timeline = document.querySelector('.scroll-timeline');
|
||||
const fill = document.querySelector('.timeline-progress-fill');
|
||||
const nav = document.querySelector('.quarter-nav');
|
||||
const sidebarGoals = window.gsap.utils.toArray('.sidebar-goal-item');
|
||||
const quarterSelectWrap = document.querySelector('.quarter-select-wrap');
|
||||
const header = document.querySelector('header');
|
||||
const MO_BREAKPOINT = 1024;
|
||||
|
||||
if (!main || !content || !sections.length) return false;
|
||||
|
||||
window.gsap.registerPlugin(window.ScrollTrigger);
|
||||
|
||||
const dots = [];
|
||||
const quarterNavItems = [];
|
||||
let quarterNavFill = null;
|
||||
let quarterSelect = null;
|
||||
let absYList = [];
|
||||
let timelineH = 0;
|
||||
let currentIndex = -1;
|
||||
let stInstances = [];
|
||||
let layoutRefreshRaf = null;
|
||||
|
||||
function getHeaderHeight() {
|
||||
return header ? header.offsetHeight : 0;
|
||||
}
|
||||
|
||||
function setHeights() {
|
||||
const isMo = window.innerWidth <= MO_BREAKPOINT;
|
||||
if (isMo) {
|
||||
content.style.height = '';
|
||||
main.style.height = '';
|
||||
sections.forEach(function (section) { section.style.height = ''; });
|
||||
return;
|
||||
}
|
||||
|
||||
content.style.height = '';
|
||||
let h = Math.round(content.getBoundingClientRect().height || content.clientHeight);
|
||||
if (!h || h < 1) {
|
||||
h = window.innerHeight - getHeaderHeight();
|
||||
}
|
||||
main.style.height = h + 'px';
|
||||
sections.forEach(function (section) { section.style.height = h + 'px'; });
|
||||
}
|
||||
|
||||
function buildTimelineDots() {
|
||||
if (!timeline) return;
|
||||
timeline.querySelectorAll('.timeline-dot').forEach(function (dot) { dot.remove(); });
|
||||
sections.forEach(function (section, index) {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'timeline-dot' + (index === 0 ? ' is-active' : '');
|
||||
dot.dataset.index = String(index);
|
||||
if (!section.querySelector('.sidebar-quarter')) {
|
||||
dot.classList.add('timeline-dot--triangle');
|
||||
dot.innerHTML = '<span class="ico-quarter-triangle" aria-hidden="true"></span>';
|
||||
}
|
||||
timeline.appendChild(dot);
|
||||
dots.push(dot);
|
||||
});
|
||||
}
|
||||
|
||||
function buildQuarterSelect() {
|
||||
if (!quarterSelectWrap) return;
|
||||
quarterSelectWrap.innerHTML = '';
|
||||
const select = document.createElement('select');
|
||||
select.className = 'quarter-select';
|
||||
select.setAttribute('aria-label', '분기 선택');
|
||||
|
||||
sections.forEach(function (section, index) {
|
||||
const quarter = section.dataset.quarter || String(index + 1);
|
||||
const quarterEl = section.querySelector('.sidebar-quarter');
|
||||
const label = quarterEl
|
||||
? quarterEl.innerText.replace(/\s+/g, ' ').trim()
|
||||
: quarter + '분기';
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = String(index);
|
||||
option.textContent = label;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.addEventListener('change', function () {
|
||||
const idx = parseInt(this.value, 10);
|
||||
if (Number.isNaN(idx)) return;
|
||||
main.scrollTo({ left: idx * main.offsetWidth, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
quarterSelectWrap.appendChild(select);
|
||||
quarterSelect = select;
|
||||
}
|
||||
|
||||
function buildQuarterNav() {
|
||||
if (!nav) return;
|
||||
nav.innerHTML = '';
|
||||
|
||||
const trackEl = document.createElement('div');
|
||||
trackEl.className = 'quarter-nav-track';
|
||||
trackEl.setAttribute('aria-hidden', 'true');
|
||||
trackEl.innerHTML = '<div class="quarter-nav-fill"></div>';
|
||||
nav.appendChild(trackEl);
|
||||
quarterNavFill = trackEl.querySelector('.quarter-nav-fill');
|
||||
|
||||
sections.forEach(function (section, index) {
|
||||
const quarter = section.dataset.quarter || String(index + 1);
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'quarter-nav-item' + (index === 0 ? ' is-active' : '');
|
||||
button.setAttribute('aria-label', quarter + '분기로 이동');
|
||||
button.innerHTML =
|
||||
'<span class="quarter-nav-dot" aria-hidden="true"></span>' +
|
||||
'<span class="quarter-nav-label">' + quarter + '분기</span>';
|
||||
|
||||
button.addEventListener('click', function () {
|
||||
if (window.innerWidth <= MO_BREAKPOINT) {
|
||||
main.scrollTo({ left: index * main.offsetWidth, behavior: 'smooth' });
|
||||
} else {
|
||||
main.scrollTo({ top: index * main.offsetHeight, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
nav.appendChild(button);
|
||||
quarterNavItems.push(button);
|
||||
});
|
||||
}
|
||||
|
||||
function positionTimeline() {
|
||||
if (!timeline || !sections.length) return;
|
||||
|
||||
const sectionH = main.clientHeight;
|
||||
const halfDot = 6;
|
||||
const savedScroll = main.scrollTop;
|
||||
main.scrollTop = 0;
|
||||
|
||||
const mainRect = main.getBoundingClientRect();
|
||||
absYList = sections.map(function (section, index) {
|
||||
const anchor = section.querySelector('.sidebar-quarter') || section.querySelector('.sidebar-goal-icon');
|
||||
if (!anchor) return index * sectionH;
|
||||
const sectionRect = section.getBoundingClientRect();
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const offsetInSection = anchorRect.top + anchorRect.height / 2 - sectionRect.top;
|
||||
return index * sectionH + offsetInSection;
|
||||
});
|
||||
|
||||
const sidebar = sections[0].querySelector('.scroll-section-sidebar');
|
||||
const sidebarRect = sidebar ? sidebar.getBoundingClientRect() : mainRect;
|
||||
const timelineLeft = (sidebarRect.left - mainRect.left) + 6;
|
||||
|
||||
main.scrollTop = savedScroll;
|
||||
|
||||
const firstY = absYList[0];
|
||||
const lastY = absYList[absYList.length - 1];
|
||||
timelineH = Math.max(lastY - firstY, 0);
|
||||
|
||||
timeline.style.top = firstY + 'px';
|
||||
timeline.style.left = timelineLeft + 'px';
|
||||
timeline.style.height = timelineH + 'px';
|
||||
|
||||
dots.forEach(function (dot, index) {
|
||||
dot.style.top = (absYList[index] - firstY - halfDot) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function positionQuarterNav() {
|
||||
if (!nav || quarterNavItems.length < 2) return;
|
||||
const trackEl = nav.querySelector('.quarter-nav-track');
|
||||
if (!trackEl) return;
|
||||
|
||||
const firstDot = quarterNavItems[0].querySelector('.quarter-nav-dot');
|
||||
const lastDot = quarterNavItems[quarterNavItems.length - 1].querySelector('.quarter-nav-dot');
|
||||
if (!firstDot || !lastDot) return;
|
||||
|
||||
const navRect = nav.getBoundingClientRect();
|
||||
const fRect = firstDot.getBoundingClientRect();
|
||||
const lRect = lastDot.getBoundingClientRect();
|
||||
|
||||
if (window.innerWidth <= MO_BREAKPOINT) {
|
||||
const top = fRect.top + fRect.height / 2 - navRect.top;
|
||||
const left = fRect.left + fRect.width / 2 - navRect.left;
|
||||
const width = (lRect.left + lRect.width / 2 - navRect.left) - left;
|
||||
trackEl.style.top = top + 'px';
|
||||
trackEl.style.left = left + 'px';
|
||||
trackEl.style.width = Math.max(width, 0) + 'px';
|
||||
trackEl.style.height = '';
|
||||
} else {
|
||||
const top = fRect.top + fRect.height / 2 - navRect.top;
|
||||
const left = fRect.left + fRect.width / 2 - navRect.left;
|
||||
const height = (lRect.top + lRect.height / 2 - navRect.top) - top;
|
||||
trackEl.style.top = top + 'px';
|
||||
trackEl.style.left = left + 'px';
|
||||
trackEl.style.height = Math.max(height, 0) + 'px';
|
||||
trackEl.style.width = '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const isMo = window.innerWidth <= MO_BREAKPOINT;
|
||||
const sectionH = isMo ? main.clientWidth : main.clientHeight;
|
||||
const scrollPos = isMo ? main.scrollLeft : main.scrollTop;
|
||||
let fillPct = 0;
|
||||
|
||||
if (absYList.length >= 2 && timelineH > 0) {
|
||||
const rawIdx = scrollPos / sectionH;
|
||||
const i0 = Math.max(0, Math.min(Math.floor(rawIdx), absYList.length - 2));
|
||||
const i1 = i0 + 1;
|
||||
const t = rawIdx - i0;
|
||||
const pos0 = absYList[i0] - absYList[0];
|
||||
const pos1 = absYList[i1] - absYList[0];
|
||||
const fillPx = pos0 + t * (pos1 - pos0);
|
||||
fillPct = Math.min((fillPx / timelineH) * 100, 100);
|
||||
}
|
||||
|
||||
const pct = fillPct + '%';
|
||||
if (fill) fill.style.height = pct;
|
||||
if (quarterNavFill) {
|
||||
if (isMo) quarterNavFill.style.width = pct;
|
||||
else quarterNavFill.style.height = pct;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshLayoutAfterHeaderChange() {
|
||||
if (layoutRefreshRaf) cancelAnimationFrame(layoutRefreshRaf);
|
||||
layoutRefreshRaf = requestAnimationFrame(function () {
|
||||
setHeights();
|
||||
positionTimeline();
|
||||
positionQuarterNav();
|
||||
updateProgress();
|
||||
if (typeof window.ScrollTrigger !== 'undefined') {
|
||||
window.ScrollTrigger.refresh();
|
||||
}
|
||||
layoutRefreshRaf = null;
|
||||
});
|
||||
}
|
||||
|
||||
function updateActive(index) {
|
||||
if (index === currentIndex) return;
|
||||
currentIndex = index;
|
||||
|
||||
sidebarGoals.forEach(function (goal, i) {
|
||||
goal.classList.toggle('is-active', i === index);
|
||||
});
|
||||
dots.forEach(function (dot, i) {
|
||||
dot.classList.toggle('is-active', i <= index);
|
||||
});
|
||||
quarterNavItems.forEach(function (item, i) {
|
||||
item.classList.toggle('is-active', i <= index);
|
||||
});
|
||||
|
||||
if (quarterSelect && window.innerWidth <= MO_BREAKPOINT) {
|
||||
quarterSelect.value = String(index);
|
||||
}
|
||||
|
||||
const page = document.querySelector('.myclass-page');
|
||||
if (page) {
|
||||
const shouldHideIntro = index > 0;
|
||||
const prevHidden = page.classList.contains('intro-hidden');
|
||||
page.classList.toggle('intro-hidden', shouldHideIntro);
|
||||
if (prevHidden !== shouldHideIntro) {
|
||||
refreshLayoutAfterHeaderChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initGSAPTriggers() {
|
||||
stInstances.forEach(function (st) { st.kill(); });
|
||||
stInstances = [];
|
||||
window.ScrollTrigger.clearScrollMemory();
|
||||
|
||||
setHeights();
|
||||
positionTimeline();
|
||||
|
||||
if (window.innerWidth <= MO_BREAKPOINT) return;
|
||||
|
||||
window.ScrollTrigger.defaults({ scroller: main });
|
||||
sections.forEach(function (section, index) {
|
||||
stInstances.push(
|
||||
window.ScrollTrigger.create({
|
||||
trigger: section,
|
||||
start: 'top center',
|
||||
end: 'bottom center',
|
||||
onEnter: function () { updateActive(index); },
|
||||
onEnterBack: function () { updateActive(index); },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
buildTimelineDots();
|
||||
buildQuarterSelect();
|
||||
buildQuarterNav();
|
||||
setHeights();
|
||||
positionTimeline();
|
||||
positionQuarterNav();
|
||||
updateActive(0);
|
||||
updateProgress();
|
||||
|
||||
const revealSections = document.querySelectorAll('.scroll-section-reveal');
|
||||
if (revealSections.length && 'IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
setTimeout(function () { entry.target.classList.add('is-visible'); }, 80);
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
}, { root: main, rootMargin: '0px', threshold: 0.15 });
|
||||
revealSections.forEach(function (section) { observer.observe(section); });
|
||||
} else {
|
||||
revealSections.forEach(function (section) { section.classList.add('is-visible'); });
|
||||
}
|
||||
|
||||
main.addEventListener('scroll', function () {
|
||||
updateProgress();
|
||||
const isMo = window.innerWidth <= MO_BREAKPOINT;
|
||||
const idx = isMo
|
||||
? Math.round(main.scrollLeft / main.clientWidth)
|
||||
: Math.round(main.scrollTop / main.clientHeight);
|
||||
updateActive(Math.min(idx, sections.length - 1));
|
||||
}, { passive: true });
|
||||
|
||||
let swipeStartX = 0;
|
||||
let swipeStartY = 0;
|
||||
main.addEventListener('touchstart', function (event) {
|
||||
if (window.innerWidth > MO_BREAKPOINT) return;
|
||||
swipeStartX = event.touches[0].clientX;
|
||||
swipeStartY = event.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
|
||||
main.addEventListener('touchend', function (event) {
|
||||
if (window.innerWidth > MO_BREAKPOINT) return;
|
||||
const dx = event.changedTouches[0].clientX - swipeStartX;
|
||||
const dy = event.changedTouches[0].clientY - swipeStartY;
|
||||
const hasHorizontalSwipe = Math.abs(dx) >= 40 && Math.abs(dx) >= Math.abs(dy);
|
||||
let idx = Math.round(main.scrollLeft / main.clientWidth);
|
||||
|
||||
if (hasHorizontalSwipe) {
|
||||
if (dx < 0) idx = Math.min(idx + 1, sections.length - 1);
|
||||
else idx = Math.max(idx - 1, 0);
|
||||
main.scrollTo({ left: idx * main.clientWidth, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
const syncIdx = Math.round(main.scrollLeft / main.clientWidth);
|
||||
currentIndex = -1;
|
||||
updateActive(Math.min(syncIdx, sections.length - 1));
|
||||
}, hasHorizontalSwipe ? 350 : 200);
|
||||
}, { passive: true });
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
setHeights();
|
||||
positionTimeline();
|
||||
positionQuarterNav();
|
||||
initGSAPTriggers();
|
||||
updateProgress();
|
||||
});
|
||||
|
||||
let resizeTimer = null;
|
||||
let prevIsMo = window.innerWidth <= MO_BREAKPOINT;
|
||||
let prevMainHeight = Math.round(main.getBoundingClientRect().height || 0);
|
||||
window.addEventListener('resize', function () {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
const nextIsMo = window.innerWidth <= MO_BREAKPOINT;
|
||||
|
||||
setHeights();
|
||||
positionTimeline();
|
||||
positionQuarterNav();
|
||||
updateProgress();
|
||||
|
||||
const nextMainHeight = Math.round(main.getBoundingClientRect().height || 0);
|
||||
const shouldRefresh = (nextIsMo !== prevIsMo) || (Math.abs(nextMainHeight - prevMainHeight) > 1);
|
||||
if (shouldRefresh && typeof window.ScrollTrigger !== 'undefined') {
|
||||
window.ScrollTrigger.refresh();
|
||||
}
|
||||
|
||||
prevIsMo = nextIsMo;
|
||||
prevMainHeight = nextMainHeight;
|
||||
}, 120);
|
||||
});
|
||||
|
||||
initGSAPTriggers();
|
||||
return true;
|
||||
}
|
||||
|
||||
function initQuarterNavigation() {
|
||||
const sections = Array.from(document.querySelectorAll('.scroll-section'));
|
||||
const navs = Array.from(document.querySelectorAll('.quarter-nav'));
|
||||
const main = document.querySelector('.myclass-main');
|
||||
|
||||
if (!sections.length || !navs.length || !main) return;
|
||||
|
||||
navs.forEach(function (nav) {
|
||||
nav.innerHTML = '';
|
||||
sections.forEach(function (section, index) {
|
||||
const label = section.querySelector('.sidebar-quarter strong:last-child');
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'quarter-nav-item' + (index === 0 ? ' is-active' : '');
|
||||
button.innerHTML = '<span class="quarter-nav-dot" aria-hidden="true"></span><span class="quarter-nav-label">' + (label ? label.textContent : String(index + 1)) + '분기</span>';
|
||||
button.addEventListener('click', function () {
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
nav.appendChild(button);
|
||||
});
|
||||
});
|
||||
|
||||
function updateActiveQuarter() {
|
||||
const mainTop = main.getBoundingClientRect().top;
|
||||
let activeIndex = 0;
|
||||
|
||||
sections.forEach(function (section, index) {
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (rect.top - mainTop <= 120) {
|
||||
activeIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
navs.forEach(function (nav) {
|
||||
nav.querySelectorAll('.quarter-nav-item').forEach(function (item, index) {
|
||||
item.classList.toggle('is-active', index === activeIndex);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main.addEventListener('scroll', updateActiveQuarter, { passive: true });
|
||||
window.addEventListener('resize', updateActiveQuarter);
|
||||
updateActiveQuarter();
|
||||
}
|
||||
|
||||
function initTimeline() {
|
||||
const timeline = document.querySelector('.scroll-timeline');
|
||||
const main = document.querySelector('.myclass-main');
|
||||
const sections = Array.from(document.querySelectorAll('.scroll-section'));
|
||||
const fill = document.querySelector('.timeline-progress-fill');
|
||||
|
||||
if (!timeline || !main || !sections.length || !fill) return;
|
||||
|
||||
const dots = [];
|
||||
|
||||
sections.forEach(function (_section, index) {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'timeline-dot' + (index === 0 ? ' is-active' : '');
|
||||
timeline.appendChild(dot);
|
||||
dots.push(dot);
|
||||
});
|
||||
|
||||
function position() {
|
||||
const firstRect = sections[0].getBoundingClientRect();
|
||||
const lastRect = sections[sections.length - 1].getBoundingClientRect();
|
||||
const mainRect = main.getBoundingClientRect();
|
||||
const start = firstRect.top - mainRect.top + 80;
|
||||
const end = lastRect.top - mainRect.top + 80;
|
||||
const height = Math.max(end - start, 0);
|
||||
|
||||
timeline.style.top = start + 'px';
|
||||
timeline.style.height = height + 'px';
|
||||
|
||||
dots.forEach(function (dot, index) {
|
||||
const sectionRect = sections[index].getBoundingClientRect();
|
||||
const offset = sectionRect.top - mainRect.top + 80 - start;
|
||||
dot.style.top = offset + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function update() {
|
||||
const scrollMax = Math.max(main.scrollHeight - main.clientHeight, 1);
|
||||
const ratio = Math.min(main.scrollTop / scrollMax, 1);
|
||||
fill.style.height = ratio * 100 + '%';
|
||||
|
||||
let activeIndex = 0;
|
||||
sections.forEach(function (section, index) {
|
||||
if (section.offsetTop - main.scrollTop <= 120) {
|
||||
activeIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
dots.forEach(function (dot, index) {
|
||||
dot.classList.toggle('is-active', index <= activeIndex);
|
||||
});
|
||||
}
|
||||
|
||||
main.addEventListener('scroll', update, { passive: true });
|
||||
window.addEventListener('resize', function () {
|
||||
position();
|
||||
update();
|
||||
});
|
||||
|
||||
position();
|
||||
update();
|
||||
}
|
||||
|
||||
initLottieIcons();
|
||||
initRankToggle();
|
||||
initButtons();
|
||||
initVideoModal();
|
||||
if (!initGsapScrollEvents()) {
|
||||
ensureSectionRevealVisible();
|
||||
initQuarterNavigation();
|
||||
initTimeline();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user