Files
edu/skin/Copy of index.php

533 lines
22 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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="/edu/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="/edu/js/main/VideoCardRenderer.js" defer></script>
<script src="/edu/js/main/VideoSlider.js" defer></script>
<script src="/edu/js/main/Videomodalmanager.js" defer></script>
<script src="/edu/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('/edu/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 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 activeMyKeywords = [...allowKeywords];
let activeAdminKeywords = [...allAdminKwList];
console.log('[KEYWORDS INIT] activeMyKeywords:', activeMyKeywords, ', 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 = allowKeywords.filter((k) => _stored.myActive.includes(k));
activeMyKeywords = 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);
keywordTagList.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
const label = cb.closest('label');
const kw = getKeywordFromLabelText(label?.textContent || '');
cb.checked = selected.has(kw);
});
}
function renderMyKeywords(selectedKeywords) {
const myKeywordEl = document.getElementById('myKeyword');
if (!myKeywordEl) return;
myKeywordEl.innerHTML = selectedKeywords.map((kw, idx) => `\n<label class="kw-allow" for="chk_my_${idx}">\n <input type="checkbox" id="chk_my_${idx}" checked />\n #${kw}\n</label>`).join('');
}
function openKeywordModal() {
if (!keywordModal) return;
ensureModalKeywords();
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 selectedMy = Array.from(keywordTagList.querySelectorAll('input[type="checkbox"]'))
.filter((cb) => cb.checked)
.map((cb) => getKeywordFromLabelText(cb.closest('label')?.textContent || ''))
.filter(Boolean)
.slice(0, 3); // 최대 3개
// 0개 포함 항상 저장 (사용자가 모두 해제한 경우도 반영)
activeMyKeywords = Array.from(new Set(selectedMy));
renderMyKeywords(activeMyKeywords);
saveActiveState();
// DB 저장 (백그라운드)
fetch('/edu/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 || !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 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('/edu/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 gaugeTotalMin = <?= (int)$totalMin ?>;//추가
console.warn('[gaugeMaxValue]', gaugeMaxValue);
console.warn('[gaugeTotalMin]', gaugeTotalMin);
// const initialGaugeMinutes = Math.min(gaugeMaxValue, <?= (int)$totalMin ?>);//기존
const initialGaugeMinutes = Math.max(gaugeMaxValue, <?= (int)$totalMin ?>);//변경
let effectiveMaxValue = Math.max(gaugeMaxValue, gaugeTotalMin);//추가
console.warn('[initialGaugeMinutes]', initialGaugeMinutes);//추가
//const gauge = new GaugeChart({ size: 832, strokeWidth: 31, maxValue: gaugeMaxValue, padding: 20, outerTextOffset: 6, innerTextOffset: 35, dotRadius: 7 });//기존
const gauge = new GaugeChart({ size: 832, strokeWidth: 31, maxValue: effectiveMaxValue, padding: 20, outerTextOffset: 6, innerTextOffset: 35, dotRadius: 7 });//변경
gauge.init();
//gauge.update(initialGaugeMinutes);//기존
gauge.update(gaugeTotalMin);//변경
function saveActiveState() {
localStorage.setItem(ACTIVE_KEY, JSON.stringify({
myActive: activeMyKeywords,
adminActive: activeAdminKeywords,
}));
}
async function fetchAndRefreshVideos() {
try {
console.log('[fetchAndRefreshVideos] activeMyKeywords=' + JSON.stringify(activeMyKeywords) + ', activeAdminKeywords=' + JSON.stringify(activeAdminKeywords));
const res = await fetch('/edu/bbs/api/videos_by_keywords.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
my_keywords: activeMyKeywords,
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);
// API 응답에서 최신 평균값 반영
const apiAvgWatchMin = Number.parseInt(data.avg_watch_min, 10);
if (Number.isFinite(apiAvgWatchMin) && apiAvgWatchMin > 0) {
gaugeMaxValue = apiAvgWatchMin;
if (gauge && gauge.config) {
gauge.config.maxValue = gaugeMaxValue;
}
}
let gaugeMinutes = initialGaugeMinutes;
if (Number.isFinite(apiTotalSec)) {
gaugeMinutes = Math.floor(Math.max(0, apiTotalSec) / 60);
} else if (Number.isFinite(apiTotalMin)) {
gaugeMinutes = Math.max(0, apiTotalMin);
}
/*
gaugeMinutes = Math.min(gaugeMaxValue, gaugeMinutes);
gauge.update(gaugeMinutes);
*/
//----------------------------
// [수정 후] 26.03.30
// 1. 만약 내 학습시간(gaugeMinutes)이 평균(gaugeMaxValue)보다 크다면, 차트의 최대치를 내 시간에 맞춤
if (gaugeMinutes > gaugeMaxValue) {
gauge.config.maxValue = gaugeMinutes;
// 차트 라이브러리에 따라 maxValue를 변경 후 다시 그리거나 init해야 할 수 있습니다.
}
gauge.update(gaugeMinutes);
//----------------------------
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 (e.target.checked) {
if (!activeMyKeywords.includes(kw)) activeMyKeywords.push(kw);
} else {
activeMyKeywords = activeMyKeywords.filter((k) => k !== kw);
}
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);
saveActiveState();
fetchAndRefreshVideos();
});
});
});
</script>
</body>
</html>