706 lines
28 KiB
PHP
706 lines
28 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../bbs/auth.php';
|
||
edu_require_login();
|
||
|
||
require_once __DIR__ . '/../bbs/index.php';
|
||
?>
|
||
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||
<link rel="stylesheet" type="text/css" href="/css/main.css" />
|
||
</head>
|
||
<body>
|
||
<div class="wrap main">
|
||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||
<div class="container">
|
||
<div class="bg-circle">
|
||
<ul>
|
||
<li><div></div></li>
|
||
<li></li>
|
||
<li></li>
|
||
<li></li>
|
||
</ul>
|
||
</div>
|
||
<div class="learning-area">
|
||
<svg id="gauge" viewBox="0 0 794 460" preserveAspectRatio="xMidYMid meet"></svg>
|
||
</div>
|
||
<div class="main-contents">
|
||
<div class="text-box">
|
||
<span><em><?= htmlspecialchars($userName) ?></em> <?= htmlspecialchars($userRank) ?>님</span>
|
||
|
||
<div class="keyword-area">
|
||
<button id="keywordSettingsBtn">나의 키워드 <i class="ico-setting"></i></button>
|
||
|
||
<!-- [DEBUG] myKeywords:<?= count($myKeywords) ?>, adminKeywords:<?= count($adminKeywords) ?>, allKeywords:<?= count($allKeywords) ?>, error:<?= htmlspecialchars($mainDataError ?? '') ?> -->
|
||
<div class="keyword-list" id="myKeyword" style="display:flex;flex-wrap:wrap;gap:4px;">
|
||
<?php foreach ($myKeywords as $i => $kw): ?>
|
||
<label class="kw-allow" for="chk_my_<?= $i ?>" style="display:inline-flex;align-items:center;gap:4px;">
|
||
<input type="checkbox" id="chk_my_<?= $i ?>" checked />
|
||
#<?= htmlspecialchars($kw['keyword_name']) ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="keyword-list" style="display:flex;flex-wrap:wrap;gap:4px;">
|
||
<?php foreach ($adminKeywords as $i => $kw): ?>
|
||
<label class="kw-deny" for="chk_admin_<?= $i ?>" style="display:inline-flex;align-items:center;gap:4px;">
|
||
<input type="checkbox" id="chk_admin_<?= $i ?>" checked />
|
||
#<?= htmlspecialchars($kw['keyword_name']) ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<p><em>취향저격 영상 추천</em>드려요.</p>
|
||
</div>
|
||
|
||
<div class="video-wrap">
|
||
<div class="video-cards-container" id="videoCardsContainer"></div>
|
||
<!-- <button class="btn-prev" id="prevBtn">‹</button>
|
||
<button class="btn-next" id="nextBtn">›</button> -->
|
||
<!-- <div class="pagination" id="pagination"><span class="current">1</span> / 1</div> -->
|
||
</div>
|
||
|
||
<?php include(__DIR__ . "/_modal/keyword.php") ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/main/VideoCardRenderer.js" defer></script>
|
||
<script src="/js/main/VideoSlider.js" defer></script>
|
||
<script src="/js/main/Videomodalmanager.js" defer></script>
|
||
<script src="/js/main/Gaugechart.js" defer></script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
// DEBUG: 초기 상태 확인
|
||
console.log('[INDEX DEBUG]', {
|
||
videosJson: <?= $videosJson ?>,
|
||
myKwJson: <?= $myKwJson ?>,
|
||
totalMin: <?= $totalMin ?>,
|
||
avgWatchMin: <?= $avgWatchMin ?>,
|
||
userName: <?= json_encode($userName) ?>,
|
||
userRank: <?= json_encode($userRank) ?>,
|
||
allKeywordsCount: <?= count($allKeywords) ?>,
|
||
myKeywordsCount: <?= count($myKeywords) ?>,
|
||
});
|
||
|
||
// 상세 진단 정보 조회
|
||
fetch('/bbs/api/diagnosis.php')
|
||
.then(r => r.json())
|
||
.then(d => {
|
||
console.log('[DIAGNOSIS API]', d);
|
||
if (d.saved_keywords_count === 0) {
|
||
console.warn('⚠️ 저장된 키워드가 없습니다. 모달에서 키워드를 선택하고 저장해주세요.');
|
||
}
|
||
})
|
||
.catch(e => console.error('[DIAGNOSIS] fetch failed', e));
|
||
|
||
const videos = <?= $videosJson ?>;
|
||
let currentVideos = Array.isArray(videos) ? videos : [];
|
||
|
||
let allowKeywords = [];
|
||
document.querySelectorAll('#myKeyword .kw-allow input[type="checkbox"]').forEach((cb) => {
|
||
const label = cb.closest('label');
|
||
if (!label) return;
|
||
const kw = label.textContent.trim().replace(/^#/, '').trim();
|
||
if (kw) allowKeywords.push(kw);
|
||
});
|
||
if (allowKeywords.length === 0) {
|
||
allowKeywords = <?= $myKwJson ?>;
|
||
}
|
||
console.log('[KEYWORDS INIT] allowKeywords (from HTML or myKwJson):', allowKeywords);
|
||
|
||
const dbMyKeywords = Array.from(new Set((<?= $myKwJson ?> || []).filter(Boolean)));
|
||
console.log('[KEYWORDS INIT] dbMyKeywords (from DB):', dbMyKeywords);
|
||
|
||
const ACTIVE_KEY = 'edu_kw_active';
|
||
const allAdminKwList = Array.from(document.querySelectorAll('.kw-deny')).map((lbl) => lbl.textContent.trim().replace(/^#/, '').trim()).filter(Boolean);
|
||
console.log('[KEYWORDS INIT] allAdminKwList (from .kw-deny):', allAdminKwList);
|
||
|
||
let myKeywordsList = [...dbMyKeywords];
|
||
// 모달/DB 저장 기준 상태
|
||
let activeMyKeywords = [...dbMyKeywords];
|
||
// 페이지(메인)에서만 사용하는 임시 활성 상태
|
||
let pageActiveMyKeywords = [...allowKeywords];
|
||
let activeAdminKeywords = [...allAdminKwList];
|
||
let excludedByAdminKeywords = [];
|
||
console.log('[KEYWORDS INIT] myKeywordsList:', myKeywordsList, ', activeMyKeywords:', activeMyKeywords, ', pageActiveMyKeywords:', pageActiveMyKeywords, ', activeAdminKeywords:', activeAdminKeywords);
|
||
// [DEBUG] 관리자 추천 키워드 HTML 렌더링 확인
|
||
console.log('[ADMIN KEYWORDS FINAL]', {
|
||
count: document.querySelectorAll('.kw-deny').length,
|
||
keywords: Array.from(document.querySelectorAll('.kw-deny')).map(lbl => lbl.textContent.trim()),
|
||
});
|
||
const _stored = JSON.parse(localStorage.getItem(ACTIVE_KEY) || 'null');
|
||
if (_stored) {
|
||
if (Array.isArray(_stored.myActive)) {
|
||
const filtered = myKeywordsList.filter((k) => _stored.myActive.includes(k));
|
||
pageActiveMyKeywords = filtered;
|
||
}
|
||
if (Array.isArray(_stored.adminActive)) {
|
||
const filtered = allAdminKwList.filter((k) => _stored.adminActive.includes(k));
|
||
if (filtered.length > 0) activeAdminKeywords = filtered;
|
||
}
|
||
}
|
||
|
||
const keywordModal = document.getElementById('keywordModal');
|
||
const keywordSettingsBtn = document.getElementById('keywordSettingsBtn');
|
||
const keywordModalCloseBtn = keywordModal?.querySelector('.btn-close');
|
||
const keywordTagList = keywordModal?.querySelector('.keyword-tag');
|
||
|
||
function getKeywordFromLabelText(text) {
|
||
return String(text || '').trim().replace(/^#/, '').trim();
|
||
}
|
||
|
||
function getModalKeywordLabels() {
|
||
if (!keywordTagList) return [];
|
||
return Array.from(keywordTagList.querySelectorAll('label'))
|
||
.map((label) => getKeywordFromLabelText(label.textContent))
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function ensureModalKeywords() {
|
||
if (!keywordTagList) return;
|
||
|
||
const existing = getModalKeywordLabels();
|
||
if (existing.length > 0) return;
|
||
|
||
const fallbackKeywords = Array.from(new Set([
|
||
...allowKeywords,
|
||
...allAdminKwList,
|
||
...activeMyKeywords,
|
||
])).filter(Boolean);
|
||
|
||
keywordTagList.innerHTML = fallbackKeywords.map((kw, idx) => {
|
||
const safeId = `kwtag_fallback_${idx + 1}`;
|
||
return `\n<li class="kw-box">\n <label for="${safeId}">\n <input type="checkbox" id="${safeId}" />\n #${kw}\n </label>\n</li>`;
|
||
}).join('');
|
||
}
|
||
|
||
function syncModalSelection() {
|
||
if (!keywordTagList) return;
|
||
const selected = new Set(activeMyKeywords);
|
||
const adminSet = new Set(activeAdminKeywords);
|
||
keywordTagList.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
||
const label = cb.closest('label');
|
||
const kw = getKeywordFromLabelText(label?.textContent || '');
|
||
const blocked = adminSet.has(kw);
|
||
cb.checked = !blocked && selected.has(kw);
|
||
});
|
||
}
|
||
|
||
function normalizeMyKeywordsByAdmin() {
|
||
const adminSet = new Set(activeAdminKeywords);
|
||
const overlaps = activeMyKeywords.filter((kw) => adminSet.has(kw));
|
||
excludedByAdminKeywords = Array.from(new Set(overlaps));
|
||
activeMyKeywords = activeMyKeywords.filter((kw) => !adminSet.has(kw));
|
||
return overlaps;
|
||
}
|
||
|
||
function applyModalAdminKeywordRestrictions() {
|
||
if (!keywordTagList) return;
|
||
const adminSet = new Set(activeAdminKeywords);
|
||
|
||
keywordTagList.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
||
const label = cb.closest('label');
|
||
const kw = getKeywordFromLabelText(label?.textContent || '');
|
||
const blocked = adminSet.has(kw);
|
||
|
||
if (blocked) {
|
||
cb.checked = false;
|
||
}
|
||
|
||
cb.disabled = blocked;
|
||
if (label) {
|
||
label.style.opacity = blocked ? '0.45' : '';
|
||
label.style.cursor = blocked ? 'not-allowed' : '';
|
||
if (blocked) {
|
||
label.setAttribute('title', '회사 추천 키워드는 나의 키워드로 선택할 수 없습니다.');
|
||
} else {
|
||
label.removeAttribute('title');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderMyKeywords(allKeywords, selectedKeywords, excludedKeywords = []) {
|
||
const myKeywordEl = document.getElementById('myKeyword');
|
||
if (!myKeywordEl) return;
|
||
|
||
const selectedSet = new Set(selectedKeywords);
|
||
const excludedSet = new Set(excludedKeywords);
|
||
|
||
const normalHtml = allKeywords
|
||
.filter((kw) => !excludedSet.has(kw))
|
||
.map((kw, idx) => `\n<label class="kw-allow" for="chk_my_${idx}">\n <input type="checkbox" id="chk_my_${idx}" ${selectedSet.has(kw) ? 'checked' : ''} />\n #${kw}\n</label>`)
|
||
.join('');
|
||
|
||
const excludedHtml = allKeywords
|
||
.filter((kw) => excludedSet.has(kw))
|
||
.map((kw, idx) => `\n<label class="kw-allow kw-excluded" for="chk_my_ex_${idx}" title="회사 추천 키워드와 중복되어 제외됨" style="opacity:.45;">\n <input type="checkbox" id="chk_my_ex_${idx}" disabled />\n #${kw} (제외)\n</label>`)
|
||
.join('');
|
||
|
||
myKeywordEl.innerHTML = normalHtml + excludedHtml;
|
||
}
|
||
|
||
function openKeywordModal() {
|
||
if (!keywordModal) return;
|
||
ensureModalKeywords();
|
||
applyModalAdminKeywordRestrictions();
|
||
syncModalSelection();
|
||
keywordModal.classList.add('is-open');
|
||
keywordModal.style.display = 'block';
|
||
keywordModal.setAttribute('aria-hidden', 'false');
|
||
document.body.classList.add('modal-open');
|
||
// 접근성: 모달 열릴 때 닫기 버튼으로 포커스 이동
|
||
keywordModalCloseBtn?.focus();
|
||
}
|
||
|
||
function closeKeywordModal(save = false) {
|
||
if (!keywordModal) return;
|
||
|
||
if (save && keywordTagList) {
|
||
const adminSet = new Set(activeAdminKeywords);
|
||
const selectedMy = Array.from(keywordTagList.querySelectorAll('input[type="checkbox"]'))
|
||
.filter((cb) => cb.checked && !cb.disabled)
|
||
.map((cb) => getKeywordFromLabelText(cb.closest('label')?.textContent || ''))
|
||
.filter((kw) => !adminSet.has(kw))
|
||
.filter(Boolean)
|
||
.slice(0, 3); // 최대 3개
|
||
|
||
// 0개 포함 항상 저장 (사용자가 모두 해제한 경우도 반영)
|
||
myKeywordsList = Array.from(new Set(selectedMy));
|
||
activeMyKeywords = [...myKeywordsList];
|
||
normalizeMyKeywordsByAdmin();
|
||
pageActiveMyKeywords = [...activeMyKeywords];
|
||
renderMyKeywords(myKeywordsList, pageActiveMyKeywords, excludedByAdminKeywords);
|
||
saveActiveState();
|
||
// DB 저장 (백그라운드)
|
||
fetch('/bbs/api/user_keywords.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ keywords: activeMyKeywords }),
|
||
}).then(async (res) => {
|
||
const d = await res.json().catch(() => ({}));
|
||
if (!d.success) console.warn('[user-keywords save] 실패', d);
|
||
}).catch((e) => console.warn('[user-keywords save]', e));
|
||
fetchAndRefreshVideos();
|
||
}
|
||
|
||
// 접근성: aria-hidden 설정 전에 반드시 포커스를 모달 밖으로 이동
|
||
keywordSettingsBtn?.focus();
|
||
keywordModal.classList.remove('is-open');
|
||
keywordModal.style.display = 'none';
|
||
keywordModal.setAttribute('aria-hidden', 'true');
|
||
document.body.classList.remove('modal-open');
|
||
}
|
||
|
||
keywordSettingsBtn?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
openKeywordModal();
|
||
});
|
||
|
||
keywordModalCloseBtn?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
closeKeywordModal(true);
|
||
});
|
||
|
||
keywordModal?.addEventListener('click', (e) => {
|
||
if (e.target === keywordModal) {
|
||
closeKeywordModal(false);
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && keywordModal?.classList.contains('is-open')) {
|
||
closeKeywordModal(false);
|
||
}
|
||
});
|
||
|
||
// 키워드 모달 체크박스 3개 제한
|
||
const MAX_KEYWORDS = 3;
|
||
keywordTagList?.addEventListener('change', (e) => {
|
||
const cb = e.target.closest('input[type="checkbox"]');
|
||
if (!cb) return;
|
||
|
||
if (cb.disabled) {
|
||
cb.checked = false;
|
||
alert('회사 추천 키워드는 나의 키워드로 선택할 수 없습니다.');
|
||
return;
|
||
}
|
||
|
||
if (!cb.checked) return;
|
||
const checked = keywordTagList.querySelectorAll('input[type="checkbox"]:checked');
|
||
if (checked.length > MAX_KEYWORDS) {
|
||
cb.checked = false;
|
||
alert('키워드는 최대 ' + MAX_KEYWORDS + '개까지 선택 가능합니다.');
|
||
}
|
||
});
|
||
|
||
let renderer = new VideoCardRenderer({ animationDelay: 50 });
|
||
let slider = new VideoSlider({
|
||
videos: currentVideos,
|
||
videosPerPage: 6,
|
||
onPageChange: (pageVideos) => renderer.renderCards(pageVideos),
|
||
});
|
||
let modalManager = new VideoModalManager({ videos: currentVideos });
|
||
|
||
slider.init();
|
||
renderer.renderCards(slider.getCurrentPageVideos());
|
||
modalManager.init();
|
||
|
||
// 모바일에서 화면(뷰포트) 밖으로 벗어난 카드 흐리게 처리
|
||
(function initMobileCardViewportDimming() {
|
||
const container = document.getElementById('videoCardsContainer');
|
||
if (!container) return;
|
||
|
||
const mq = window.matchMedia('(max-width: 1023px)');
|
||
const DIM_CLASS = 'is-outside-viewport';
|
||
const FULLY_VISIBLE_RATIO = 0.98;
|
||
|
||
let io = null;
|
||
let mo = null;
|
||
|
||
function clearAllDims() {
|
||
container.querySelectorAll('.video-card.' + DIM_CLASS).forEach((el) => el.classList.remove(DIM_CLASS));
|
||
}
|
||
|
||
function ensureIntersectionObserver() {
|
||
if (io) return io;
|
||
io = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry) => {
|
||
const el = entry.target;
|
||
const ratio = typeof entry.intersectionRatio === 'number' ? entry.intersectionRatio : 0;
|
||
const fullyVisible = entry.isIntersecting && ratio >= FULLY_VISIBLE_RATIO;
|
||
el.classList.toggle(DIM_CLASS, !fullyVisible);
|
||
});
|
||
}, {
|
||
root: null, // viewport 기준
|
||
threshold: [0, 0.01, 0.5, FULLY_VISIBLE_RATIO, 1],
|
||
});
|
||
return io;
|
||
}
|
||
|
||
function observeAllCards() {
|
||
if (!mq.matches) return;
|
||
const observer = ensureIntersectionObserver();
|
||
container.querySelectorAll('.video-card').forEach((card) => observer.observe(card));
|
||
}
|
||
|
||
function disconnectObservers() {
|
||
if (io) {
|
||
io.disconnect();
|
||
io = null;
|
||
}
|
||
if (mo) {
|
||
mo.disconnect();
|
||
mo = null;
|
||
}
|
||
}
|
||
|
||
function enable() {
|
||
disconnectObservers();
|
||
clearAllDims();
|
||
observeAllCards();
|
||
|
||
mo = new MutationObserver(() => {
|
||
// 카드 재렌더링(페이지 변경/필터 변경) 시 재관측
|
||
observeAllCards();
|
||
});
|
||
mo.observe(container, { childList: true, subtree: true });
|
||
}
|
||
|
||
function disable() {
|
||
disconnectObservers();
|
||
clearAllDims();
|
||
}
|
||
|
||
function sync() {
|
||
if (mq.matches) enable();
|
||
else disable();
|
||
}
|
||
|
||
// 최초 적용 + 반응형 전환 대응
|
||
sync();
|
||
if (typeof mq.addEventListener === 'function') {
|
||
mq.addEventListener('change', sync);
|
||
} else if (typeof mq.addListener === 'function') {
|
||
mq.addListener(sync);
|
||
}
|
||
})();
|
||
|
||
function extractYouTubeId(url) {
|
||
const raw = String(url || '').trim();
|
||
const matched = raw.match(/(?:v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{11})/);
|
||
if (matched && matched[1]) return matched[1];
|
||
if (/^[A-Za-z0-9_-]{11}$/.test(raw)) return raw;
|
||
return '';
|
||
}
|
||
|
||
function bindCardOpenFallback() {
|
||
const container = document.getElementById('videoCardsContainer');
|
||
if (!container || container.dataset.modalFallbackBound === '1') return;
|
||
|
||
container.dataset.modalFallbackBound = '1';
|
||
container.addEventListener('click', (e) => {
|
||
if (e.defaultPrevented) return;
|
||
|
||
const card = e.target.closest('.card[data-video-id]');
|
||
if (!card || !container.contains(card)) return;
|
||
|
||
e.preventDefault();
|
||
const videoId = card.getAttribute('data-video-id');
|
||
if (!videoId) return;
|
||
|
||
try {
|
||
if (modalManager && typeof modalManager.openVideo === 'function') {
|
||
modalManager.openVideo(videoId);
|
||
return;
|
||
}
|
||
} catch (err) {
|
||
console.warn('[video-open fallback] modal open failed', err);
|
||
}
|
||
|
||
const selected = (Array.isArray(currentVideos) ? currentVideos : []).find((item) => String(item.id) === String(videoId));
|
||
const ytId = extractYouTubeId(selected && selected.url ? selected.url : '');
|
||
if (ytId) {
|
||
window.open(`https://www.youtube.com/watch?v=${ytId}`, '_blank', 'noopener,noreferrer');
|
||
}
|
||
});
|
||
}
|
||
|
||
async function saveWishlist(videoId, isActive) {
|
||
const params = new URLSearchParams({
|
||
content_id: String(videoId || ''),
|
||
is_active: isActive ? '1' : '0',
|
||
});
|
||
|
||
const res = await fetch('/bbs/api/save_wishlist.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
body: params.toString(),
|
||
});
|
||
|
||
return res.json();
|
||
}
|
||
|
||
function setBookmarkState(videoId, isActive) {
|
||
currentVideos = (Array.isArray(currentVideos) ? currentVideos : []).map((v) => {
|
||
if (String(v.id) === String(videoId)) {
|
||
return { ...v, bookmark: !!isActive };
|
||
}
|
||
return v;
|
||
});
|
||
|
||
document.querySelectorAll(`#videoCardsContainer .card[data-video-id="${String(videoId)}"]`).forEach((card) => {
|
||
const cb = card.querySelector('.bookmark input[type="checkbox"]');
|
||
if (cb) cb.checked = !!isActive;
|
||
});
|
||
}
|
||
|
||
function bindWishlistEvents() {
|
||
const container = document.getElementById('videoCardsContainer');
|
||
if (!container || container.dataset.wishlistBound === '1') return;
|
||
container.dataset.wishlistBound = '1';
|
||
|
||
container.addEventListener('click', (e) => {
|
||
// 하트 클릭 시 카드 오픈만 막고, 체크 토글은 허용해야 change 이벤트가 발생함
|
||
if (e.target.closest('.bookmark')) {
|
||
e.stopPropagation();
|
||
}
|
||
});
|
||
|
||
container.addEventListener('change', async (e) => {
|
||
const checkbox = e.target.closest('.bookmark input[type="checkbox"]');
|
||
if (!checkbox) return;
|
||
|
||
const card = checkbox.closest('.card[data-video-id]');
|
||
const videoId = card?.getAttribute('data-video-id');
|
||
if (!videoId) return;
|
||
|
||
const nextState = !!checkbox.checked;
|
||
checkbox.disabled = true;
|
||
|
||
try {
|
||
const result = await saveWishlist(videoId, nextState);
|
||
if (!result || !result.success) {
|
||
checkbox.checked = !nextState;
|
||
console.warn('[wishlist] save failed', {
|
||
videoId,
|
||
nextState,
|
||
result,
|
||
});
|
||
return;
|
||
}
|
||
setBookmarkState(videoId, nextState);
|
||
} catch (err) {
|
||
checkbox.checked = !nextState;
|
||
console.warn('[wishlist]', err?.message || err);
|
||
} finally {
|
||
checkbox.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
bindCardOpenFallback();
|
||
bindWishlistEvents();
|
||
|
||
let gaugeMaxValue = <?= (int)$avgWatchMin ?>;
|
||
let currentGaugeLabelMinutes = <?= (int)$totalMin ?>;
|
||
const initialGaugeMinutes = Math.min(gaugeMaxValue, currentGaugeLabelMinutes);
|
||
const gauge = new GaugeChart({ size: 832, strokeWidth: 31, maxValue: gaugeMaxValue, padding: 20, outerTextOffset: 6, innerTextOffset: 35, dotRadius: 7 });
|
||
gauge.init();
|
||
gauge.update(initialGaugeMinutes, currentGaugeLabelMinutes);
|
||
|
||
// 전체 평균 학습시간은 마이페이지 기준 API로 동기화한다.
|
||
async function syncGaugeAverageFromMypage() {
|
||
try {
|
||
const res = await fetch('/ajax/get_init_data_for_mypage.php');
|
||
const data = await res.json();
|
||
if (!data || !data.success) return;
|
||
|
||
const avgTotalSeconds = Number.parseInt(data.avg_total_minutes, 10);
|
||
const avgTotalMinutes = Number.isFinite(avgTotalSeconds)
|
||
? Math.floor(Math.max(0, avgTotalSeconds) / 60)
|
||
: 0;
|
||
|
||
if (avgTotalMinutes <= 0) return;
|
||
|
||
gaugeMaxValue = avgTotalMinutes;
|
||
if (gauge && gauge.config) {
|
||
gauge.config.maxValue = gaugeMaxValue;
|
||
}
|
||
|
||
const gaugeMinutes = Math.min(gaugeMaxValue, currentGaugeLabelMinutes);
|
||
gauge.update(gaugeMinutes, currentGaugeLabelMinutes);
|
||
} catch (e) {
|
||
console.warn('[mypage avg sync]', e?.message || e);
|
||
}
|
||
}
|
||
|
||
syncGaugeAverageFromMypage();
|
||
|
||
function saveActiveState() {
|
||
localStorage.setItem(ACTIVE_KEY, JSON.stringify({
|
||
myActive: pageActiveMyKeywords,
|
||
adminActive: activeAdminKeywords,
|
||
}));
|
||
}
|
||
|
||
async function fetchAndRefreshVideos() {
|
||
try {
|
||
console.log('[fetchAndRefreshVideos] pageActiveMyKeywords=' + JSON.stringify(pageActiveMyKeywords) + ', activeAdminKeywords=' + JSON.stringify(activeAdminKeywords));
|
||
const res = await fetch('/bbs/api/videos_by_keywords.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
my_keywords: pageActiveMyKeywords,
|
||
admin_keywords: activeAdminKeywords,
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
console.log('[videos_by_keywords response]', data);
|
||
if (!data.success) {
|
||
console.error('[videos_by_keywords] API 실패', data.error);
|
||
return;
|
||
}
|
||
|
||
currentVideos = Array.isArray(data.videos) ? data.videos : [];
|
||
|
||
const apiTotalSec = Number.parseInt(data.total_watch_tm ?? data.total_all_tm, 10);
|
||
const apiTotalMin = Number.parseInt(data.total_min, 10);
|
||
|
||
let gaugeLabelMinutes = initialGaugeMinutes;
|
||
if (Number.isFinite(apiTotalSec)) {
|
||
gaugeLabelMinutes = Math.floor(Math.max(0, apiTotalSec) / 60);
|
||
} else if (Number.isFinite(apiTotalMin)) {
|
||
gaugeLabelMinutes = Math.max(0, apiTotalMin);
|
||
}
|
||
currentGaugeLabelMinutes = gaugeLabelMinutes;
|
||
const gaugeMinutes = Math.min(gaugeMaxValue, gaugeLabelMinutes);
|
||
gauge.update(gaugeMinutes, gaugeLabelMinutes);
|
||
|
||
slider = new VideoSlider({
|
||
videos: currentVideos,
|
||
videosPerPage: 6,
|
||
onPageChange: (pv) => renderer.renderCards(pv),
|
||
});
|
||
slider.init();
|
||
renderer.renderCards(slider.getCurrentPageVideos());
|
||
|
||
if (modalManager) {
|
||
modalManager.config.videos = currentVideos;
|
||
}
|
||
bindCardOpenFallback();
|
||
bindWishlistEvents();
|
||
} catch (e) {
|
||
console.warn('[키워드 필터]', e.message);
|
||
}
|
||
}
|
||
|
||
// 페이지 진입 시에도 watch_tm 기반 최신 누적값으로 게이지 갱신
|
||
fetchAndRefreshVideos();
|
||
|
||
document.getElementById('myKeyword')?.addEventListener('change', (e) => {
|
||
if (e.target.type !== 'checkbox') return;
|
||
const lbl = e.target.closest('label.kw-allow');
|
||
if (!lbl) return;
|
||
const kw = lbl.textContent.trim().replace(/^#/, '').trim();
|
||
if (activeAdminKeywords.includes(kw)) {
|
||
e.target.checked = false;
|
||
return;
|
||
}
|
||
if (e.target.checked) {
|
||
if (!pageActiveMyKeywords.includes(kw)) pageActiveMyKeywords.push(kw);
|
||
} else {
|
||
pageActiveMyKeywords = pageActiveMyKeywords.filter((k) => k !== kw);
|
||
}
|
||
const adminSet = new Set(activeAdminKeywords);
|
||
excludedByAdminKeywords = Array.from(new Set(pageActiveMyKeywords.filter((k) => adminSet.has(k))));
|
||
pageActiveMyKeywords = pageActiveMyKeywords.filter((k) => !adminSet.has(k));
|
||
renderMyKeywords(myKeywordsList, pageActiveMyKeywords, excludedByAdminKeywords);
|
||
saveActiveState();
|
||
fetchAndRefreshVideos();
|
||
});
|
||
|
||
document.querySelectorAll('.kw-deny input[type="checkbox"]').forEach((cb) => {
|
||
cb.addEventListener('change', function () {
|
||
const adminCbs = document.querySelectorAll('.kw-deny input[type="checkbox"]');
|
||
const checkedCount = Array.from(adminCbs).filter((c) => c.checked).length;
|
||
if (checkedCount === 0) {
|
||
this.checked = true;
|
||
return;
|
||
}
|
||
activeAdminKeywords = Array.from(adminCbs)
|
||
.filter((c) => c.checked)
|
||
.map((c) => c.closest('label')?.textContent.trim().replace(/^#/, '').trim())
|
||
.filter(Boolean);
|
||
|
||
const adminSet = new Set(activeAdminKeywords);
|
||
const overlaps = pageActiveMyKeywords.filter((kw) => adminSet.has(kw));
|
||
excludedByAdminKeywords = Array.from(new Set(overlaps));
|
||
pageActiveMyKeywords = pageActiveMyKeywords.filter((kw) => !adminSet.has(kw));
|
||
renderMyKeywords(myKeywordsList, pageActiveMyKeywords, excludedByAdminKeywords);
|
||
applyModalAdminKeywordRestrictions();
|
||
syncModalSelection();
|
||
if (overlaps.length > 0) {
|
||
console.log('[키워드 중복 제외] 회사 추천과 중복되어 제외됨:', overlaps);
|
||
}
|
||
|
||
saveActiveState();
|
||
fetchAndRefreshVideos();
|
||
});
|
||
});
|
||
|
||
normalizeMyKeywordsByAdmin();
|
||
const initAdminSet = new Set(activeAdminKeywords);
|
||
excludedByAdminKeywords = Array.from(new Set(pageActiveMyKeywords.filter((kw) => initAdminSet.has(kw))));
|
||
pageActiveMyKeywords = pageActiveMyKeywords.filter((kw) => !initAdminSet.has(kw));
|
||
renderMyKeywords(myKeywordsList, pageActiveMyKeywords, excludedByAdminKeywords);
|
||
});
|
||
</script>
|
||
|
||
<?php //include(__DIR__ . '/guide.php') ?>
|
||
</body>
|
||
</html>
|