Files
edu/skin/index_moon.php
T

422 lines
17 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
/**
* skin/index.php — 메인 페이지 뷰 템플릿
*
* DB 조회 로직은 bbs/main_data.php 에 위임한다.
* 이 파일은 HTML 출력(뷰)만 담당한다.
*
* bbs/main_data.php 가 제공하는 변수:
* $userName - 사용자 이름
* $userRank - 직위
* $myKeywords - 내 키워드 배열 (max 3)
* $adminKeywords - 관리자 추천 키워드 배열 (max 2)
* $allKeywords - 모달용 전체 키워드 목록
* $videosJson - 영상 6개 JSON 문자열
* $myKwJson - 내 키워드명 JSON 문자열
* $totalMin - 총 학습시간(분)
*/
require_once __DIR__ . '/../bbs/main_data.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") ?>
<!-- container -->
<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">
<!-- 사용자명 / 직위 (bbs/main_data.php 제공) -->
<span><em><?= htmlspecialchars($userName) ?></em> <?= htmlspecialchars($userRank) ?>님</span>
<div class="keyword-area">
<button id="keywordSettingsBtn">
나의 키워드 <i class="ico-setting"></i>
</button>
<!-- 내 키워드 3개 — 상단 (bbs/main_data.php 제공, 항상 값 있음) -->
<div class="keyword-list" id="myKeyword">
<?php foreach ($myKeywords as $i => $kw): ?>
<label class="kw-allow" for="chk_my_<?= $i ?>">
<input type="checkbox" id="chk_my_<?= $i ?>" checked />
#<?= htmlspecialchars($kw['keyword_name']) ?>
</label>
<?php endforeach; ?>
</div>
<!-- 관리자 추천 키워드 2개 — 하단 (bbs/main_data.php 제공, 항상 값 있음) -->
<div class="keyword-list">
<?php foreach ($adminKeywords as $i => $kw): ?>
<label class="kw-deny" for="chk_admin_<?= $i ?>">
<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>
<!-- keyword modal (bbs/main_data.php 의 $allKeywords, $myKeywords 참조) -->
<?php include(__DIR__ . "/_modal/keyword.php") ?>
<!-- // keyword modal -->
</div>
</div>
<!-- // container -->
</div>
<!-- 메인 페이지 스크립트 (의존성 순서 유지하며 defer로 비동기 로드) -->
<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>
// defer 스크립트가 로드된 후 실행되도록 DOMContentLoaded 사용
document.addEventListener("DOMContentLoaded", function () {
// ── 영상 데이터 (bbs/main_data.php → PHP → JSON 주입) ──────
// 슬롯: [0]=Pick(좌상) [1~2]=관리자키워드(좌중하) [3~5]=내키워드(우)
const videos = <?= $videosJson ?>;
// ── 내 키워드 초기값 ─────────────────────────────────────
// PHP에서 렌더링된 DOM에서 직접 읽어 JS와 HTML을 항상 일치시킴
// (DB 데이터/fallback 여부와 무관하게 화면에 보이는 값이 기준)
let allowKeywords = [];
document.querySelectorAll('#myKeyword .kw-allow input[type="checkbox"]').forEach((cb) => {
const label = cb.closest('label');
if (label) {
const kw = label.textContent.trim().replace(/^#/, '').trim();
if (kw) allowKeywords.push(kw);
}
});
// PHP JSON을 보조 참조로 사용 (DOM 읽기 실패 시 fallback)
if (allowKeywords.length === 0) {
allowKeywords = <?= $myKwJson ?>;
}
const maxAllowKeywords = 3;
// ── 활성 키워드 초기 상태 (localStorage 복원) ────────────────
const ACTIVE_KEY = 'edu_kw_active';
const allAdminKwList = Array.from(document.querySelectorAll('.kw-deny')).map(
(lbl) => lbl.textContent.trim().replace(/^#/, '').trim()
).filter(Boolean);
let activeMyKeywords = [...allowKeywords];
let activeAdminKeywords = [...allAdminKwList];
const _stored = JSON.parse(localStorage.getItem(ACTIVE_KEY) || 'null');
if (_stored) {
if (_stored.myActive) {
const f = allowKeywords.filter((k) => _stored.myActive.includes(k));
if (f.length > 0) activeMyKeywords = f;
}
if (_stored.adminActive) {
const f = allAdminKwList.filter((k) => _stored.adminActive.includes(k));
activeAdminKeywords = f.length > 0 ? f : allAdminKwList.slice(0, 1);
}
}
// 초기 체크박스 상태 반영
document.querySelectorAll('#myKeyword .kw-allow').forEach((lbl) => {
const kw = lbl.textContent.trim().replace(/^#/, '').trim();
const cb = lbl.querySelector('input[type="checkbox"]');
if (cb) cb.checked = activeMyKeywords.includes(kw);
});
document.querySelectorAll('.kw-deny').forEach((lbl) => {
const kw = lbl.textContent.trim().replace(/^#/, '').trim();
const cb = lbl.querySelector('input[type="checkbox"]');
if (cb) cb.checked = activeAdminKeywords.includes(kw);
});
// ── 렌더러 / 슬라이더 / 모달 초기화 ──────────────────────
let renderer = new VideoCardRenderer({ animationDelay: 50 });
let slider = new VideoSlider({
videos: videos,
videosPerPage: 6,
onPageChange: (pageVideos) => {
renderer.renderCards(pageVideos);
},
});
let modalManager = new VideoModalManager({ videos: videos });
slider.init();
renderer.renderCards(slider.getCurrentPageVideos());
modalManager.init();
// ── 초기 로드 시 localStorage 활성 상태 반영 ──────────────
// PHP 서버 렌더링은 전체 키워드 기준이므로,
// 저장된 활성 상태가 있으면 즉시 API 호출로 교체
if (_stored) {
fetchAndRefreshVideos();
}
// ── 게이지 차트 초기화 ────────────────────────────────────
const gauge = new GaugeChart({
size: 832,
strokeWidth: 31,
maxValue: 50,
padding: 20,
outerTextOffset: 6,
innerTextOffset: 35,
dotRadius: 7,
});
gauge.init();
gauge.update(<?= min(50, (int)$totalMin) ?>); // DB 총 학습분 (50분 상한)
// ════════════════════════════════════════════════════════
// 키워드 모달 기능
// ════════════════════════════════════════════════════════
const modal = document.getElementById("keywordModal");
const settingsBtn = document.getElementById("keywordSettingsBtn");
const checkIcon = modal ? modal.querySelector(".modal-header .btn-close") : null;
// 모달 체크박스 상태 동기화
function syncModalCheckboxes() {
modal.querySelectorAll(".keyword-tag input[type='checkbox']").forEach((cb) => {
const label = cb.closest("label");
if (!label) return;
const kwText = label.textContent.trim().replace(/^#/, "").trim();
cb.checked = allowKeywords.includes(kwText);
});
}
// 최대 선택 시 나머지 비활성화
function updateCheckboxStates() {
const isMaxReached = allowKeywords.length >= maxAllowKeywords;
modal.querySelectorAll(".keyword-tag input[type='checkbox']").forEach((cb) => {
const label = cb.closest("label");
if (!label) return;
const kwText = label.textContent.trim().replace(/^#/, "").trim();
const selected = allowKeywords.includes(kwText);
cb.disabled = !selected && isMaxReached;
label.style.opacity = (!selected && isMaxReached) ? "0.5" : "1";
label.style.cursor = (!selected && isMaxReached) ? "not-allowed" : "pointer";
});
}
// 메인 화면 키워드 표시 갱신
function updateMainKeywordDisplay() {
const area = document.querySelector("#myKeyword");
if (!area) return;
area.innerHTML = "";
allowKeywords.forEach((kw, i) => {
const label = document.createElement("label");
label.className = "kw-allow";
label.setAttribute("for", `chk_allow_${i}`);
const cb = document.createElement("input");
cb.type = "checkbox";
cb.id = `chk_allow_${i}`;
cb.checked = activeMyKeywords.includes(kw);
label.appendChild(cb);
label.appendChild(document.createTextNode(`#${kw}`));
area.appendChild(label);
});
}
// 한도 초과 안내 메시지
function showLimitMessage() {
const header = modal.querySelector(".modal-header");
if (!header || header.querySelector(".limit-message")) return;
const msg = document.createElement("span");
msg.className = "limit-message";
msg.textContent = ` (최대 ${maxAllowKeywords}개까지 선택 가능)`;
msg.style.cssText = "color:#ff4444;font-size:14px;margin-left:10px;";
header.appendChild(msg);
setTimeout(() => msg.remove(), 2000);
}
// 키워드 저장 → edu/bbs/api/user_keywords.php 호출
async function saveKeywords(keywords) {
try {
const res = await fetch("/bbs/api/user_keywords.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ keywords }),
});
if (!res.ok) throw new Error("저장 실패");
} catch (e) {
console.warn("[키워드 저장]", e.message);
}
}
// 모달 열기
if (settingsBtn) {
settingsBtn.onclick = () => {
syncModalCheckboxes();
updateCheckboxStates();
modal.style.opacity = "";
modal.style.display = "block";
};
}
// 모달 외부 클릭 시 닫기
if (modal) {
modal.onclick = (e) => {
if (e.target === modal) {
modal.style.display = "none";
updateMainKeywordDisplay();
saveKeywords(allowKeywords);
}
};
}
// 닫기(체크) 버튼
if (checkIcon) {
checkIcon.onclick = () => {
modal.style.display = "none";
updateMainKeywordDisplay();
saveKeywords(allowKeywords);
};
}
// 모달 체크박스 변경 이벤트
modal && modal.querySelectorAll(".keyword-tag input[type='checkbox']").forEach((cb) => {
cb.onchange = () => {
const label = cb.closest("label");
if (!label) return;
const kwText = label.textContent.trim().replace(/^#/, "").trim();
if (cb.checked && allowKeywords.length >= maxAllowKeywords) {
cb.checked = false;
showLimitMessage();
return;
}
if (!cb.disabled) {
if (cb.checked) {
if (!allowKeywords.includes(kwText)) allowKeywords.push(kwText);
} else {
allowKeywords = allowKeywords.filter((k) => k !== kwText);
}
updateCheckboxStates();
}
};
});
// ── 활성 상태 localStorage 저장 ────────────────────────────
function saveActiveState() {
localStorage.setItem(ACTIVE_KEY, JSON.stringify({
myActive: activeMyKeywords,
adminActive: activeAdminKeywords,
}));
}
// ── 키워드 변경 시 영상 새로고침 (AJAX) ──────────────────────
async function fetchAndRefreshVideos() {
try {
const res = await fetch('/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();
if (!data.success) return;
slider = new VideoSlider({
videos: data.videos,
videosPerPage: 6,
onPageChange: (pv) => renderer.renderCards(pv),
});
slider.init();
renderer.renderCards(slider.getCurrentPageVideos());
modalManager = new VideoModalManager({ videos: data.videos });
modalManager.init();
} catch (e) {
console.warn('[키워드 필터]', e.message);
}
}
// ── 내 키워드 (kw-allow) 토글 — 이벤트 위임 ─────────────────
// updateMainKeywordDisplay() 가 DOM을 재생성하므로 위임 방식 사용
const myKwContainer = document.getElementById('myKeyword');
if (myKwContainer) {
myKwContainer.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();
});
}
// ── 회사 추천 키워드 (kw-deny) 토글 — 최소 1개 강제 ─────────
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;
showAdminKeywordLimitMessage();
return;
}
activeAdminKeywords = Array.from(adminCbs)
.filter((c) => c.checked)
.map((c) => {
const lbl = c.closest('label');
return lbl ? lbl.textContent.trim().replace(/^#/, '').trim() : '';
})
.filter(Boolean);
saveActiveState();
fetchAndRefreshVideos();
});
});
function showAdminKeywordLimitMessage() {
if (document.getElementById('adminKwLimitMsg')) return;
const area = document.querySelector('.keyword-area');
if (!area) return;
const msg = document.createElement('span');
msg.id = 'adminKwLimitMsg';
msg.textContent = '회사 추천 키워드는 최소 1개 이상 선택해야 합니다.';
msg.style.cssText = 'display:block;color:#ff4444;font-size:12px;margin-top:4px;';
area.appendChild(msg);
setTimeout(() => { const el = document.getElementById('adminKwLimitMsg'); if (el) el.remove(); }, 2000);
}
}); // DOMContentLoaded 종료
</script>
</body>
</html>