/** * 마이클래스 페이지 전용 스크립트 * - Lottie 아이콘, 순위/연도 토글, 목표 선택 팝업(Swiper) */ document.addEventListener('DOMContentLoaded', function () { // ==================================================== // [1] Lottie 아이콘 초기화 (.lottie-icon) // - 목표 카드/팝업 아이콘 애니메이션, prefers-reduced-motion 시 비활성 // ==================================================== const initLottieIconsIn = (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 s = document.createElement('script'); s.src = './assets/js/lib/lottie.min.js'; s.async = true; s.dataset.lottieLoader = 'true'; s.addEventListener('load', function () { resolve(typeof window.lottie !== 'undefined'); }); s.addEventListener('error', function () { resolve(false); }); document.head.appendChild(s); }); } const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; function initOne(el) { const url = el.getAttribute('data-lottie-url'); if (!url) return; if (el.dataset.lottieInited === 'true') return; const fallbackImg = el.parentElement ? el.parentElement.querySelector('.lottie-fallback') : null; el.dataset.lottieInited = 'true'; try { if (el.__lottieAnim && typeof el.__lottieAnim.destroy === 'function') { el.__lottieAnim.destroy(); el.__lottieAnim = null; } const anim = window.lottie.loadAnimation({ container: el, renderer: 'svg', loop: !reduceMotion, autoplay: !reduceMotion, path: url, rendererSettings: { progressiveLoad: true }, }); el.__lottieAnim = anim; if (fallbackImg) fallbackImg.style.display = 'none'; anim.addEventListener('data_failed', function () { el.dataset.lottieInited = 'false'; if (fallbackImg) fallbackImg.style.display = ''; }); } catch (e) { el.dataset.lottieInited = 'false'; if (fallbackImg) fallbackImg.style.display = ''; } } function initIn(rootEl, options) { var root = rootEl && rootEl.querySelectorAll ? rootEl : document; var opts = options || {}; var force = !!opts.force; var scopedIcons = root.querySelectorAll('.lottie-icon[data-lottie-url]'); if (force) { scopedIcons.forEach(function (el) { el.dataset.lottieInited = 'false'; el.innerHTML = ''; }); } return loadLottieScript().then(function (ok) { if (!ok || typeof window.lottie === 'undefined') return; scopedIcons.forEach(initOne); }); } initIn(document); return initIn; })(); // ==================================================== // [2] 순위 목록 토글 // - 상단 트로피 버튼 클릭 시 1~5위 팝업 열기/닫기, 바깥 클릭 시 닫기 // ==================================================== const btnRankToggle = document.querySelector('.btn-rank-toggle'); const rankListPopup = document.getElementById('rankListPopup'); if (btnRankToggle && rankListPopup) { btnRankToggle.addEventListener('click', function () { const isOpen = this.classList.toggle('is-open'); rankListPopup.hidden = !isOpen; this.setAttribute('aria-expanded', isOpen); }); document.addEventListener('click', function (e) { if (btnRankToggle.classList.contains('is-open') && !btnRankToggle.contains(e.target) && !rankListPopup.contains(e.target)) { btnRankToggle.classList.remove('is-open'); rankListPopup.hidden = true; btnRankToggle.setAttribute('aria-expanded', 'false'); } }); } // ==================================================== // [3] 연도 선택 셀렉트 // - 연도 버튼 클릭 시 연도 목록 팝업, 선택 시 .year-display 갱신 // ==================================================== const btnYearToggle = document.getElementById('btnYearToggle'); const yearListPopup = document.getElementById('yearListPopup'); const yearDisplay = document.querySelector('.year-display'); const yearDisplayStrong = yearDisplay?.querySelector('strong'); function setYearDisplay(year) { if (!yearDisplay || !yearDisplayStrong) return; const prefix = String(year).slice(0, -2); const suffix = String(year).slice(-2); const textNode = Array.from(yearDisplay.childNodes).find(function (n) { return n.nodeType === Node.TEXT_NODE; }); if (textNode) { textNode.textContent = prefix; } else { yearDisplay.insertBefore(document.createTextNode(prefix), yearDisplayStrong); } yearDisplayStrong.textContent = suffix; } function closeYearSelect() { if (btnYearToggle) { btnYearToggle.classList.remove('is-open'); btnYearToggle.setAttribute('aria-expanded', 'false'); } if (yearListPopup) yearListPopup.hidden = true; } if (btnYearToggle && yearListPopup && yearDisplay) { btnYearToggle.addEventListener('click', function () { const isOpen = this.classList.toggle('is-open'); yearListPopup.hidden = !isOpen; this.setAttribute('aria-expanded', isOpen); }); yearListPopup.querySelectorAll('.year-option').forEach(function (opt) { opt.addEventListener('click', function () { const year = this.dataset.year; setYearDisplay(year); closeYearSelect(); }); }); document.addEventListener('click', function (e) { if (btnYearToggle.classList.contains('is-open') && !btnYearToggle.contains(e.target) && !yearListPopup.contains(e.target)) { closeYearSelect(); } }); } // ==================================================== // [4] 목표 데이터 (HTML의 window.MYCLASS_GOALS에서 로드) // - id, title, desc; goalCardColors는 슬라이드 카드 배경색 // ==================================================== const goals = (window.MYCLASS_GOALS && Array.isArray(window.MYCLASS_GOALS)) ? window.MYCLASS_GOALS : []; const goalRecs = (window.MYCLASS_GOAL_RECS && typeof window.MYCLASS_GOAL_RECS === 'object') ? window.MYCLASS_GOAL_RECS : {}; const goalBooks = (window.MYCLASS_GOAL_BOOKS && typeof window.MYCLASS_GOAL_BOOKS === 'object') ? window.MYCLASS_GOAL_BOOKS : {}; const goalCodeMap = (window.MYCLASS_GOAL_CODE_MAP && typeof window.MYCLASS_GOAL_CODE_MAP === 'object') ? window.MYCLASS_GOAL_CODE_MAP : {}; const quarterCode = (typeof window.MYCLASS_QUARTER_CODE === 'string') ? window.MYCLASS_QUARTER_CODE : ''; let currentGoalId = null; let selectedGoalId = null; const goalLayer = document.getElementById('goalLayer'); const btnCloseLayer = document.getElementById('btnCloseLayer'); const btnSetGoal = document.getElementById('btnSetGoal'); const btnSelectGoal = document.getElementById('btnSelectGoal'); const goalSlideWrapper = document.getElementById('goalSlideWrapper'); const popupPagination = document.getElementById('popupPagination'); let goalPopupSwiper = null; // 목표별 카드 배경색 (팝업 내 비활성 슬라이드 카드용) const goalCardColors = { 1: '#46B8A5', 2: '#3EAEBC', 3: '#46B8A5', 4: '#3B9EAC', 5: '#3EB09C', 6: '#42ACBA', 7: '#3DAE9E', 8: '#40B2A2', }; function escapeHtml(text) { return String(text || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatMultilineHtml(text) { return escapeHtml(String(text || '').replace(/\\n/g, '\n')).replace(/\r?\n/g, '
'); } function populateGoalSlideBooks(slide, goalId) { const list = slide.querySelector('.goal-slide-bookshelf .books-list'); if (!list) return; const firstCard = list.querySelector('.books-item'); if (!firstCard) return; const template = firstCard.cloneNode(true); const books = goalBooks[String(goalId)] || goalBooks[goalId] || []; const renderBooks = Array.isArray(books) && books.length ? books : []; list.innerHTML = ''; if (!renderBooks.length) { list.appendChild(template); return; } renderBooks.forEach(function (book, index) { const item = template.cloneNode(true); const contentId = parseInt(book.id, 10); const videoId = Number.isNaN(contentId) ? (index + 1) : contentId; const title = String(book.title || ''); const sub = String(book.sub || ''); const main = String(book.main || ''); const youtube = String(book.youtube || ''); const thumb = String(book.thumbnail || ''); const fallbackImgName = String(book.img || 'img_book_01'); const coverDesktop = thumb || ('/img/myclass/' + fallbackImgName + '.png'); const coverMobile = thumb || ('/img/myclass/' + fallbackImgName + '_m.png'); const videoThumb = thumb || (youtube ? ('https://img.youtube.com/vi/' + youtube + '/sddefault.jpg') : coverDesktop); item.setAttribute('data-video-id', String(videoId)); const imgWrap = item.querySelector('.book-img-wrap'); if (imgWrap) { imgWrap.style.setProperty('--book-img', "url('" + coverDesktop + "')"); imgWrap.style.setProperty('--book-img-m', "url('" + coverMobile + "')"); } const sourceEl = item.querySelector('.book-img-picture source'); const imageEl = item.querySelector('.book-img-picture img'); if (sourceEl) sourceEl.setAttribute('srcset', coverMobile); if (imageEl) imageEl.setAttribute('src', coverDesktop); const infoBtn = item.querySelector('.book-info-btn'); if (infoBtn) { infoBtn.setAttribute('data-video-id', String(videoId)); infoBtn.setAttribute('aria-label', '영상 상세 보기 - ' + title); } const videoThumbEl = item.querySelector('.book-video-thumb'); if (videoThumbEl) { videoThumbEl.setAttribute('src', videoThumb); } const bookmarkLabel = item.querySelector('.bookmark'); const bookmarkInput = bookmarkLabel ? bookmarkLabel.querySelector('input[type="checkbox"]') : null; if (bookmarkLabel && bookmarkInput) { const bookmarkId = 'like_book_goal_' + goalId + '_' + videoId + '_' + index; bookmarkInput.id = bookmarkId; bookmarkLabel.setAttribute('for', bookmarkId); } const titleEl = item.querySelector('.book-title'); const subEl = item.querySelector('.book-desc-sub'); const mainEl = item.querySelector('.book-desc-main'); if (titleEl) titleEl.textContent = title; if (subEl) subEl.innerHTML = formatMultilineHtml(sub); if (mainEl) mainEl.innerHTML = formatMultilineHtml(main); list.appendChild(item); }); } // 팝업용 슬라이드 DOM 생성 및 목표 데이터 반영 function buildGoalSlides() { const templateSlide = goalSlideWrapper && goalSlideWrapper.querySelector('.goal-slide'); if (!templateSlide) return; for (let i = 1; i < goals.length; i++) { const clone = templateSlide.cloneNode(true); clone.dataset.goalId = goals[i].id; clone.classList.remove('swiper-slide-active', 'swiper-slide-prev', 'swiper-slide-next'); goalSlideWrapper.appendChild(clone); } goals.forEach(function (goal) { const slide = goalSlideWrapper.querySelector('.goal-slide[data-goal-id="' + goal.id + '"]'); const goalItem = document.querySelector('.goal-item[data-goal-id="' + goal.id + '"]'); if (!slide) return; const color = goalCardColors[goal.id] || '#46B8A5'; slide.style.setProperty('--card-color-1', color); const titleEl = slide.querySelector('.goal-slide-title'); const descEl = slide.querySelector('.goal-slide-desc'); const iconEl = slide.querySelector('.goal-slide-icon'); if (titleEl) titleEl.textContent = goal.title; if (descEl) descEl.textContent = goal.desc; const miniTitleEl = slide.querySelector('.goal-slide-title-mini'); const miniDescEl = slide.querySelector('.goal-slide-desc-mini'); const miniIconEl = slide.querySelector('.goal-slide-icon-mini'); if (miniTitleEl) miniTitleEl.textContent = goal.title; if (miniDescEl) miniDescEl.textContent = goal.desc; const recItems = goalRecs[String(goal.id)] || goalRecs[goal.id] || []; const recTitleEls = slide.querySelectorAll('.goal-slide-recs .rec-target'); const recTextEls = slide.querySelectorAll('.goal-slide-recs .rec-text'); Array.prototype.forEach.call(recTitleEls, function (el, idx) { if (recItems[idx] && recItems[idx].title) { el.textContent = recItems[idx].title; } }); Array.prototype.forEach.call(recTextEls, function (el, idx) { if (recItems[idx] && recItems[idx].description) { el.textContent = recItems[idx].description; } }); populateGoalSlideBooks(slide, goal.id); if (goalItem) { const cardIcon = goalItem.querySelector('.card-icon'); if (iconEl) iconEl.innerHTML = cardIcon ? cardIcon.innerHTML : ''; if (miniIconEl) miniIconEl.innerHTML = cardIcon ? cardIcon.innerHTML : ''; } }); } // 목표 팝업: 활성 슬라이드 폭(100% vs 264px) 반영 후 translate 재정렬 (클릭·스와이프·페이지네이션 공통) function recenterGoalPopupSwiper(swiper, index) { if (!swiper || swiper.destroyed) return; var idx = index !== undefined && index !== null ? index : swiper.activeIndex; requestAnimationFrame(function () { swiper.update(); swiper.slideTo(idx, 0, false); }); } // 목표 팝업 내 Swiper 초기화 (슬라이드 인덱스로 열기) function initGoalPopupSwiper(slideIndex) { if (goalPopupSwiper) return; const initialIndex = Math.max(0, Number(slideIndex) || 0); goalPopupSwiper = new Swiper('.goal-popup-swiper', { slidesPerView: 'auto', spaceBetween: 16, speed: 0, followFinger: false, observer: true, observeParents: true, slideToClickedSlide: true, grabCursor: true, centeredSlides: true, initialSlide: initialIndex, pagination: { el: '#popupPagination', clickable: true, bulletClass: 'pagination-dot', bulletActiveClass: 'is-active', }, on: { slideChange: function () { currentGoalId = goals[this.activeIndex] ? goals[this.activeIndex].id : currentGoalId; }, slideChangeTransitionEnd: function () { recenterGoalPopupSwiper(this); }, }, }); } function destroyGoalPopupSwiper() { if (goalPopupSwiper) { goalPopupSwiper.destroy(true, true); goalPopupSwiper = null; } } // 목표 상세 레이어 열기 (goalId에 해당하는 슬라이드로 포커스) function openGoalLayer(goalId) { currentGoalId = goalId; goalLayer.removeAttribute('aria-hidden'); goalLayer.classList.remove('hidden'); goalLayer.classList.add('is-open'); document.body.classList.add('layer-open'); buildGoalSlides(); const goalIdNum = Number(goalId); const slideIdx = goals.findIndex(function (g) { return g.id === goalIdNum || g.id === goalId; }); const safeIndex = slideIdx >= 0 ? slideIdx : 0; const swiperEl = document.querySelector('.goal-popup-swiper'); swiperEl.classList.add('swiper-no-anim'); initGoalPopupSwiper(safeIndex); requestAnimationFrame(function () { requestAnimationFrame(function () { if (goalPopupSwiper) { recenterGoalPopupSwiper(goalPopupSwiper, safeIndex); } swiperEl.classList.remove('swiper-no-anim'); }); }); initLottieIconsIn(goalLayer, { force: true }); if (btnCloseLayer) btnCloseLayer.focus(); } // 목표 상세 레이어 닫기 (Swiper 제거, 슬라이드 DOM 정리) function closeGoalLayer() { destroyGoalPopupSwiper(); if (goalSlideWrapper) { const slides = goalSlideWrapper.querySelectorAll('.goal-slide'); slides.forEach(function (s, i) { if (i > 0) s.remove(); }); } goalLayer.setAttribute('aria-hidden', 'true'); goalLayer.classList.remove('is-open'); goalLayer.classList.add('hidden'); document.body.classList.remove('layer-open'); } // ==================================================== // [5] 목표 설정하기 // ==================================================== async function saveSelectedGoalToServer(goalId) { const goalCode = goalCodeMap[String(goalId)] || goalCodeMap[goalId] || ''; if (!goalCode) { return { ok: false, message: 'goal_code_missing' }; } const response = await fetch('/bbs/api/save_user_learning_goal.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ goal_code: goalCode, quarter: quarterCode }), }); if (!response.ok) { let failedPayload = null; try { failedPayload = await response.json(); } catch (_e) { failedPayload = null; } return { ok: false, message: (failedPayload && failedPayload.message) ? failedPayload.message : ('http_' + response.status), }; } let payload = null; try { payload = await response.json(); } catch (_e) { payload = null; } return { ok: !!(payload && payload.success), message: (payload && payload.message) ? payload.message : '', }; } if (btnSetGoal) { btnSetGoal.addEventListener('click', async function (e) { e.preventDefault(); const nextUrl = btnSetGoal.getAttribute('href') || '/skin/myclass_list.php'; selectedGoalId = currentGoalId; if (!selectedGoalId) { window.location.href = nextUrl; return; } btnSetGoal.classList.add('is-loading'); btnSetGoal.setAttribute('aria-disabled', 'true'); const saveResult = await saveSelectedGoalToServer(selectedGoalId); btnSetGoal.classList.remove('is-loading'); btnSetGoal.removeAttribute('aria-disabled'); if (!saveResult.ok) { const msg = saveResult.message ? (' (' + saveResult.message + ')') : ''; alert('목표 저장에 실패했습니다. 잠시 후 다시 시도해주세요.' + msg); return; } closeGoalLayer(); applySelectedState(selectedGoalId); window.location.href = nextUrl; }); } // ==================================================== // [6] 선택 후 상태 적용 // ==================================================== const selectedGoalSection = document.getElementById('selectedGoalSection'); const selectedGoalTitle = document.getElementById('selectedGoalTitle'); function applySelectedState(goalId) { const goal = goals.find(function (g) { return g.id === goalId; }); document.querySelectorAll('.goal-item').forEach(function (item) { item.classList.remove('is-selected'); const defaultArea = item.querySelector('.card-default-area'); if (defaultArea) defaultArea.hidden = false; }); const selectedItem = document.querySelector('.goal-item[data-goal-id="' + goalId + '"]'); if (selectedItem) { selectedItem.classList.add('is-selected'); selectedItem.hidden = true; } if (selectedGoalSection && selectedGoalTitle && goal) { selectedGoalTitle.textContent = goal.title; selectedGoalSection.classList.remove('hidden'); selectedGoalSection.hidden = false; } document.querySelectorAll('.intro-state').forEach(function (el) { const isDone = el.classList.contains('intro-state--done'); el.hidden = !isDone; el.classList.toggle('hidden', !isDone); }); if (btnSelectGoal) btnSelectGoal.textContent = '추가 목표 고르기'; var bookshelf = document.getElementById('bookshelf'); if (bookshelf) bookshelf.classList.add('has-selected'); } // ==================================================== // [7] 카드 클릭 이벤트 // ==================================================== document.querySelectorAll('.goal-card').forEach(function (card) { card.addEventListener('click', function () { const item = card.closest('.goal-item'); const goalId = parseInt(item.dataset.goalId, 10); openGoalLayer(goalId); }); }); if (btnSelectGoal) { btnSelectGoal.addEventListener('click', function () { const firstAvailable = selectedGoalId ? goals.find(function (g) { return g.id !== selectedGoalId; }) : goals[0]; if (firstAvailable) openGoalLayer(firstAvailable.id); }); } // ==================================================== // [8] 팝업 닫기: 닫기 버튼, 배경 클릭, ESC 키 // ==================================================== if (btnCloseLayer) btnCloseLayer.addEventListener('click', closeGoalLayer); goalLayer.addEventListener('click', function (e) { if (e.target === goalLayer) closeGoalLayer(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && goalLayer.classList.contains('is-open')) { closeGoalLayer(); } }); });