Files
edu/js/myclass_list.js
T

1312 lines
48 KiB
JavaScript

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);
});
}
let commentGatePollTimer = null;
function getRuntimePlaybackTimes() {
const fallback = { currentTime: 0, duration: 0 };
const playerCandidates = [
modalManager && modalManager.ytPlayer,
modalManager && modalManager.player,
modalManager && modalManager.currentPlayer,
modalManager && modalManager.videoPlayer,
modalManager && modalManager._player,
];
const player = playerCandidates.find(function (p) {
return p && typeof p.getCurrentTime === 'function' && typeof p.getDuration === 'function';
});
if (!player) return fallback;
try {
const currentTime = Math.max(0, Number.parseFloat(player.getCurrentTime()) || 0);
const duration = Math.max(0, Number.parseFloat(player.getDuration()) || 0);
return { currentTime: currentTime, duration: duration };
} catch (_e) {
return fallback;
}
}
function isCurrentVideoCompletedForComment() {
const video = modalManager && modalManager.currentVideo ? modalManager.currentVideo : null;
if (!video) return false;
const contentId = String(video.content_id || video.id || '').trim();
const baseWatchTm = Math.max(0, Number.parseInt(video.watch_tm ?? 0, 10) || 0);
const baseContentTm = Math.max(
0,
Number.parseInt(video.content_tm ?? video.duration ?? 0, 10) || 0
);
const runtime = getRuntimePlaybackTimes();
const watchTm = Math.max(baseWatchTm, runtime.currentTime);
const contentTm = Math.max(baseContentTm, runtime.duration);
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 isVideoModalOpen() {
const modalRoot = getActiveVideoModalRoot();
if (!modalRoot) return false;
if (modalRoot.classList.contains('is-open')) return true;
const style = window.getComputedStyle(modalRoot);
return style.display !== 'none' && style.visibility !== 'hidden';
}
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 startCommentGatePolling() {
if (commentGatePollTimer) return;
commentGatePollTimer = setInterval(function () {
if (!isVideoModalOpen()) {
clearInterval(commentGatePollTimer);
commentGatePollTimer = null;
return;
}
applyCommentWriteGate();
}, 1000);
}
function stopCommentGatePolling() {
if (!commentGatePollTimer) return;
clearInterval(commentGatePollTimer);
commentGatePollTimer = null;
}
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();
startCommentGatePolling();
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
const result = originalOpenVideo(...args);
if (result && typeof result.finally === 'function') {
return result.finally(function () {
// 모달 재사용 시 비동기 렌더 구간 전체에서 초기화
scheduleCommentDraftReset(getActiveVideoModalRoot());
scheduleCommentRefresh();
scheduleCommentGateApply();
startCommentGatePolling();
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
});
}
scheduleCommentDraftReset(getActiveVideoModalRoot());
scheduleCommentRefresh();
scheduleCommentGateApply();
startCommentGatePolling();
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();
startCommentGatePolling();
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
const result = originalLoadVideoModal(...args);
if (result && typeof result.finally === 'function') {
return result.finally(function () {
scheduleCommentDraftReset(getActiveVideoModalRoot());
scheduleCommentRefresh();
scheduleCommentGateApply();
startCommentGatePolling();
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
});
}
scheduleCommentDraftReset(getActiveVideoModalRoot());
scheduleCommentRefresh();
scheduleCommentGateApply();
startCommentGatePolling();
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) {
stopCommentGatePolling();
// 닫기 전, 시청 완료 여부 확인
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 = '<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(forceMeasure) {
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 resnapDesktopMainToIndex(idx) {
if (window.innerWidth <= MO_BREAKPOINT) return;
const h = main.clientHeight;
if (!h || !sections.length || idx < 0) return;
const clamped = Math.max(0, Math.min(idx, sections.length - 1));
main.scrollTop = clamped * h;
}
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 scrollPos = isMo ? main.scrollLeft : main.scrollTop;
let fillPct = 0;
if (absYList.length >= 2 && timelineH > 0) {
const sectionPosList = sections.map(function (section) {
return isMo ? section.offsetLeft : section.offsetTop;
});
let i0 = 0;
while (i0 < sectionPosList.length - 1 && scrollPos >= sectionPosList[i0 + 1]) i0++;
const i1 = Math.min(i0 + 1, absYList.length - 1);
let t = 0;
if (i0 !== i1) {
const segStart = sectionPosList[i0];
const segEnd = sectionPosList[i1];
const segLen = Math.max(segEnd - segStart, 1);
t = Math.min(Math.max((scrollPos - segStart) / segLen, 0), 1);
}
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(true);
positionQuarterNav();
resnapDesktopMainToIndex(currentIndex);
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(true);
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(true);
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(true);
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(true);
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();
initBookmarks();
if (!initGsapScrollEvents()) {
ensureSectionRevealVisible();
initQuarterNavigation();
initTimeline();
}
});