Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user