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 = 'CHECK!'; 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 = ''; } 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 = '
'; 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 = '' + '' + quarter + '분기'; 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 = '' + (label ? label.textContent : String(index + 1)) + '분기'; 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(); } });