Files
edu/skin/index.php
T

706 lines
28 KiB
PHP
Raw 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="/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>