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 = '/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 COMMENT_MAX_LENGTH = 200; 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(); } // --- 영상 모달 관련 이벤트 초기화 --- function clearUnsavedCommentDraft(modalRoot) { if (!modalRoot) return; const fields = modalRoot.querySelectorAll([ '.comment-box textarea', '.comment-box input[type="text"]', '.comment-write textarea', '.comment-write input[type="text"]', 'textarea[name*="comment"]', 'input[type="text"][name*="comment"]', '.comment-box [contenteditable="true"]', '.comment-write [contenteditable="true"]' ].join(',')); fields.forEach(function (field) { if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return; if (field.disabled || field.readOnly) return; if (!field.value) return; field.value = ''; field.dispatchEvent(new Event('input', { bubbles: true })); field.dispatchEvent(new Event('change', { bubbles: true })); }); modalRoot.querySelectorAll('.comment-box [contenteditable="true"], .comment-write [contenteditable="true"]').forEach(function (editable) { if (!(editable instanceof HTMLElement)) return; if (editable.getAttribute('contenteditable') !== 'true') return; if (!editable.textContent) return; editable.textContent = ''; editable.dispatchEvent(new Event('input', { bubbles: true })); editable.dispatchEvent(new Event('change', { bubbles: true })); }); } function scheduleCommentDraftReset(modalRoot) { const delays = [0, 30, 80, 160, 300, 500]; delays.forEach(function (delay) { setTimeout(function () { clearUnsavedCommentDraft(modalRoot || getActiveVideoModalRoot()); }, delay); }); } function clearRenderedCommentArea(modalRoot) { if (!modalRoot) return; modalRoot.querySelectorAll([ '.comment-list', '.comment-history', '.comment-items', '.comment-box .comment-list', '.comment-box .history-list', '.comment-box ul.comment-list' ].join(',')).forEach(function (node) { if (node instanceof HTMLElement) { node.innerHTML = ''; } }); modalRoot.querySelectorAll([ '.comment-box textarea[disabled]', '.comment-box .user-text', '.comment-list textarea', '.comment-history textarea' ].join(',')).forEach(function (node) { if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) { node.value = ''; } else if (node instanceof HTMLElement) { node.textContent = ''; } }); } function refreshRenderedComments() { const modalRoot = getActiveVideoModalRoot(); if (!modalRoot) return; clearRenderedCommentArea(modalRoot); if (typeof modalManager.setupCommentBox === 'function') { try { modalManager.setupCommentBox(); } catch (_e) {} } if (typeof modalManager.showCommentSection === 'function') { try { modalManager.showCommentSection(); } catch (_e) {} } if (typeof modalManager.adjustCommentOnlyLayout === 'function') { try { modalManager.adjustCommentOnlyLayout(); } catch (_e) {} } } function scheduleCommentRefresh() { [40, 120, 260].forEach(function (delay) { setTimeout(function () { refreshRenderedComments(); }, delay); }); } function getCommentDraftFields(modalRoot) { if (!modalRoot) return []; const selectors = [ '.comment-box textarea', '.comment-box input[type="text"]', '.comment-write textarea', '.comment-write input[type="text"]', 'textarea[name*="comment"]', 'input[type="text"][name*="comment"]' ].join(','); return Array.from(modalRoot.querySelectorAll(selectors)).filter(function (field) { if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return false; if (field.classList.contains('user-text') || field.closest('.comment-list')) return false; return true; }); } function clampCommentLength(field) { if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return; if (typeof field.value !== 'string') return; if (field.value.length <= COMMENT_MAX_LENGTH) return; field.value = field.value.slice(0, COMMENT_MAX_LENGTH); field.dispatchEvent(new Event('input', { bubbles: true })); field.dispatchEvent(new Event('change', { bubbles: true })); } function applyCommentInputLimit(modalRoot) { getCommentDraftFields(modalRoot || getActiveVideoModalRoot()).forEach(function (field) { field.maxLength = COMMENT_MAX_LENGTH; clampCommentLength(field); }); } function scheduleCommentInputLimitApply(modalRoot) { [0, 40, 120, 260, 500].forEach(function (delay) { setTimeout(function () { applyCommentInputLimit(modalRoot || getActiveVideoModalRoot()); }, delay); }); } function isCurrentVideoCompletedForComment() { const video = modalManager && modalManager.currentVideo ? modalManager.currentVideo : null; if (!video) return false; const contentId = String(video.content_id || video.id || '').trim(); const watchTm = Math.max(0, Number.parseInt(video.watch_tm ?? 0, 10) || 0); const contentTm = Math.max( 0, Number.parseInt(video.content_tm ?? video.duration ?? 0, 10) || 0 ); const completedByRatio = contentTm > 0 && watchTm >= contentTm * 0.9; const completedByFlag = !!(video.completed_at || video.completed === true); let completedByCardClass = false; if (contentId) { const card = document.querySelector('.books-item[data-video-id="' + contentId + '"]'); completedByCardClass = !!(card && card.classList.contains('books-item-completed')); } return completedByRatio || completedByFlag || completedByCardClass; } function applyCommentWriteGate() { const modalRoot = getActiveVideoModalRoot(); if (!modalRoot) return; const canWrite = isCurrentVideoCompletedForComment(); const inputSelectors = [ '.comment-box textarea', '.comment-box input[type="text"]', '.comment-write textarea', '.comment-write input[type="text"]', 'textarea[name*="comment"]', 'input[type="text"][name*="comment"]' ].join(','); modalRoot.querySelectorAll(inputSelectors).forEach(function (field) { if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return; // 읽기용 댓글 항목은 제외 if (field.classList.contains('user-text') || field.closest('.comment-list')) return; field.maxLength = COMMENT_MAX_LENGTH; field.disabled = false; field.readOnly = !canWrite; clampCommentLength(field); if (!canWrite) { field.setAttribute('placeholder', '시청 완료 후 작성 가능합니다.'); } else { field.removeAttribute('placeholder'); } }); modalRoot.querySelectorAll('.comment-box .btn-save, .comment-write .btn-save, .comment-box .btn-comment, .comment-box .btn-write').forEach(function (btn) { if (!(btn instanceof HTMLButtonElement)) return; btn.disabled = !canWrite; btn.classList.toggle('is-disabled', !canWrite); }); } function scheduleCommentGateApply() { [0, 40, 120, 260, 500].forEach(function (delay) { setTimeout(function () { applyCommentWriteGate(); }, delay); }); } function getActiveVideoModalRoot() { if (modalManager && modalManager.currentModalElement instanceof HTMLElement) { return modalManager.currentModalElement; } if (modalManager && modalManager.currentModal instanceof HTMLElement) { return modalManager.currentModal; } return document.querySelector('.modal.video.is-open, .modal.video, .video-modal.is-open, .video-modal'); } if (typeof modalManager.openVideo === 'function' && modalManager.__draftResetWrapped !== true) { const originalOpenVideo = modalManager.openVideo.bind(modalManager); modalManager.openVideo = function (...args) { scheduleCommentDraftReset(getActiveVideoModalRoot()); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(getActiveVideoModalRoot()); const result = originalOpenVideo(...args); if (result && typeof result.finally === 'function') { return result.finally(function () { // 모달 재사용 시 비동기 렌더 구간 전체에서 초기화 scheduleCommentDraftReset(getActiveVideoModalRoot()); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(getActiveVideoModalRoot()); }); } scheduleCommentDraftReset(getActiveVideoModalRoot()); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(getActiveVideoModalRoot()); return result; }; modalManager.__draftResetWrapped = true; } if (typeof modalManager.loadVideoModal === 'function' && modalManager.__draftResetLoadWrapped !== true) { const originalLoadVideoModal = modalManager.loadVideoModal.bind(modalManager); modalManager.loadVideoModal = function (...args) { scheduleCommentDraftReset(getActiveVideoModalRoot()); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(getActiveVideoModalRoot()); const result = originalLoadVideoModal(...args); if (result && typeof result.finally === 'function') { return result.finally(function () { scheduleCommentDraftReset(getActiveVideoModalRoot()); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(getActiveVideoModalRoot()); }); } scheduleCommentDraftReset(getActiveVideoModalRoot()); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(getActiveVideoModalRoot()); return result; }; modalManager.__draftResetLoadWrapped = true; } if (modalManager.__recommendedDraftResetBound !== true) { const recommendedSelectors = [ '#recommendedList .list', '.recommended-list .list', '.video-list .list', '.video-recommend-list .list', '.modal.video .list[data-video-id]', ].join(','); document.addEventListener('click', function (event) { const target = event.target; if (!target || !target.closest) return; const modalRoot = target.closest('.modal.video, .video-modal, .modal.is-open'); if (!modalRoot) return; // 페이지 카드(.books-item) 클릭과 구분하여 모달 내부 추천강의 클릭만 처리 if (target.closest('.books-item')) return; const inRecommendedArea = !!target.closest(recommendedSelectors); const hasVideoIdTarget = !!target.closest('[data-video-id]'); if (!inRecommendedArea && !hasVideoIdTarget) return; scheduleCommentDraftReset(modalRoot); scheduleCommentRefresh(); scheduleCommentGateApply(); scheduleCommentInputLimitApply(modalRoot); }, true); modalManager.__recommendedDraftResetBound = true; } if (modalManager.__commentGateSaveBlockBound !== true) { document.addEventListener('click', function (event) { const target = event.target; if (!target || !target.closest) return; const saveBtn = target.closest('.comment-box .btn-save, .comment-write .btn-save, .comment-box .btn-comment, .comment-box .btn-write'); if (!saveBtn) return; const modalRoot = saveBtn.closest('.modal.video, .video-modal, .modal.is-open'); if (!modalRoot) return; const overLimitField = getCommentDraftFields(modalRoot).find(function (field) { return (field.value || '').length > COMMENT_MAX_LENGTH; }); if (overLimitField) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); clampCommentLength(overLimitField); alert('한줄 소감문은 200자까지 입력할 수 있습니다.'); return; } if (!isCurrentVideoCompletedForComment()) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); alert('시청 완료 후 한줄 소감문 작성이 가능합니다.'); } }, true); modalManager.__commentGateSaveBlockBound = true; } if (modalManager.__commentGateInputBlockBound !== true) { let lastGateAlertAt = 0; function showGateAlertThrottled() { const now = Date.now(); if (now - lastGateAlertAt < 800) return; lastGateAlertAt = now; alert('시청 완료 후 한줄 소감문 작성이 가능합니다.'); } const inputGateSelector = [ '.comment-box textarea', '.comment-box input[type="text"]', '.comment-write textarea', '.comment-write input[type="text"]', 'textarea[name*="comment"]', 'input[type="text"][name*="comment"]' ].join(','); document.addEventListener('focusin', function (event) { const target = event.target; if (!(target instanceof HTMLElement) || !target.closest) return; const field = target.closest(inputGateSelector); if (!field) return; const modalRoot = field.closest('.modal.video, .video-modal, .modal.is-open'); if (!modalRoot) return; if (field.classList.contains('user-text') || field.closest('.comment-list')) return; if (isCurrentVideoCompletedForComment()) return; showGateAlertThrottled(); if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) { field.blur(); } }, true); document.addEventListener('click', function (event) { const target = event.target; if (!(target instanceof HTMLElement) || !target.closest) return; const field = target.closest(inputGateSelector); if (!field) return; const modalRoot = field.closest('.modal.video, .video-modal, .modal.is-open'); if (!modalRoot) return; if (field.classList.contains('user-text') || field.closest('.comment-list')) return; if (isCurrentVideoCompletedForComment()) return; event.preventDefault(); event.stopPropagation(); showGateAlertThrottled(); }, true); document.addEventListener('input', function (event) { const target = event.target; if (!(target instanceof HTMLTextAreaElement) && !(target instanceof HTMLInputElement)) return; if (!target.closest || !target.closest('.modal.video, .video-modal, .modal.is-open')) return; if (target.classList.contains('user-text') || target.closest('.comment-list')) return; if (target.value.length > COMMENT_MAX_LENGTH) { target.value = target.value.slice(0, COMMENT_MAX_LENGTH); alert('한줄 소감문은 200자까지 입력할 수 있습니다.'); } }, true); modalManager.__commentGateInputBlockBound = true; } function updateMyClassCommentIcon(contentId, hasComment) { const id = String(contentId || '').trim(); if (!id) return; const item = document.querySelector('.books-item[data-video-id="' + id + '"]'); if (!item) return; const pencil = item.querySelector('.book-ico-pencil'); if (!pencil) return; pencil.classList.toggle('book-ico-pencil-completed', !!hasComment); pencil.setAttribute('title', hasComment ? '감상문 작성 완료' : '감상문 쓰기'); } window.addEventListener('myclass:comment-status-changed', function (event) { const detail = event && event.detail ? event.detail : {}; updateMyClassCommentIcon(detail.contentId, !!detail.hasComment); }); // --- 영상모달 닫힐 때 포스트잇 즉시 표시 (새로고침 없이) --- 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 postitLines = Array.isArray(video && video.postit) ? video.postit .map(function (line) { return String(line || '').trim(); }) .filter(function (line) { return line.length > 0; }) : []; const postit = document.createElement('div'); postit.className = 'book-postit'; const title = document.createElement('strong'); title.className = 'book-postit-title'; title.textContent = 'CHECK!'; const list = document.createElement('ul'); list.className = 'book-postit-list'; (postitLines.length > 0 ? postitLines : ['영상 시청 완료']).forEach(function (line) { const li = document.createElement('li'); li.textContent = line; list.appendChild(li); }); postit.appendChild(title); postit.appendChild(list); 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 initBookmarks() { // 마이클래스 리스트 북마크 저장 처리 // 1단계: bookmarks 초기화 (MYCLASS_PAGE_VIDEOS 상태 기반) if (Array.isArray(window.MYCLASS_PAGE_VIDEOS)) { window.MYCLASS_PAGE_VIDEOS.forEach(function (video) { const contentId = String(video.id || video.content_id || '').trim(); if (!contentId) return; const checkbox = document.querySelector('.bookmark input[type="checkbox"][id*="' + contentId.replace(/[^A-Za-z0-9_-]/g, '_') + '"]'); if (checkbox) { checkbox.checked = !!video.bookmark; } }); } // 2단계: change 이벤트 바인딩 document.querySelectorAll('.bookmark input[type="checkbox"]').forEach(function (checkbox) { checkbox.addEventListener('change', async function () { const label = this.closest('.bookmark'); if (!label) return; const booksItem = label.closest('.books-item'); if (!booksItem) return; const contentId = String(booksItem.getAttribute('data-video-id') || '').trim(); if (!contentId) return; const isActive = this.checked ? '1' : '0'; try { this.disabled = true; const body = new URLSearchParams({ content_id: String(contentId), is_active: isActive, }); console.log('[myclass_list] 북마크 API 호출:', { content_id: contentId, is_active: isActive }); const response = await fetch('/bbs/api/save_wishlist.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: body.toString(), }); const responseText = await response.text(); console.log('[myclass_list] 북마크 API 응답:', responseText); let result; try { result = JSON.parse(responseText); } catch (parseErr) { console.error('[myclass_list] 북마크 API 응답 JSON 파싱 실패:', parseErr, responseText); throw new Error('서버 응답을 처리할 수 없습니다.'); } if (!response.ok || !result || result.success !== true) { console.error('[myclass_list] 북마크 API 실패:', { status: response.status, result }); throw new Error(result?.message || '북마크 저장에 실패했습니다.'); } console.log('[myclass_list] 북마크 저장 성공:', result); } catch (error) { console.error('[myclass_list] 북마크 저장 실패:', error); this.checked = !this.checked; alert(error?.message || '북마크 저장 중 오류가 발생했습니다.'); } finally { this.disabled = false; } }); }); } 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(); initBookmarks(); if (!initGsapScrollEvents()) { ensureSectionRevealVisible(); initQuarterNavigation(); initTimeline(); } });