Files

581 lines
21 KiB
JavaScript

/**
* 마이클래스 페이지 전용 스크립트
* - 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function formatMultilineHtml(text) {
return escapeHtml(String(text || '').replace(/\\n/g, '\n')).replace(/\r?\n/g, '<br>');
}
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();
}
});
});