Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
<?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>
|
||||
Binary file not shown.
@@ -0,0 +1,413 @@
|
||||
<!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">
|
||||
<span><em>홍길동</em> 선임연구원님</span>
|
||||
<div class="keyword-area">
|
||||
<button id="keywordSettingsBtn">
|
||||
나의 키워드 <i class="ico-setting"></i>
|
||||
</button>
|
||||
<div class="keyword-list" id="myKeyword">
|
||||
<label class="kw-allow" for="chk1">
|
||||
<input type="checkbox" id="chk1" />
|
||||
#인물
|
||||
</label>
|
||||
<label class="kw-allow" for="chk2">
|
||||
<input type="checkbox" id="chk2" checked />
|
||||
#소통
|
||||
</label>
|
||||
<label class="kw-allow" for="chk3">
|
||||
<input type="checkbox" id="chk3" checked />
|
||||
#협업
|
||||
</label>
|
||||
</div>
|
||||
<div class="keyword-list">
|
||||
<label class="kw-deny" for="chk4">
|
||||
<input type="checkbox" id="chk4" checked />
|
||||
#마인드셋
|
||||
</label>
|
||||
<label class="kw-deny" for="chk5">
|
||||
<input type="checkbox" id="chk5" />
|
||||
#웰니스
|
||||
</label>
|
||||
</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 -->
|
||||
<?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() {
|
||||
const videos = [
|
||||
{
|
||||
id: 1,
|
||||
url: "KE_MeQZgnPM",
|
||||
category: "리더십",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: true,
|
||||
title: "정선근 교수가 알려주는 목디스크 지식",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "comment",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: "IeF8r0ycgVg",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "꼰대가 되지 않고 건설적인 피드백을 하는 법",
|
||||
picker: "",
|
||||
type: "onboarding",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: "KMZXMI0QPoA",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업",
|
||||
picker: "",
|
||||
type: "learning",
|
||||
keywords: ["마인드셋", "중간관리자"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: "CRKwszz6l2M",
|
||||
category: "인사이트",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "프로와 아마추어를 가르는 차이점!",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: "Gf5WoZ3BmgI",
|
||||
category: "비즈트렌드",
|
||||
subcate: "성공예감",
|
||||
bookmark: false,
|
||||
title: "01/16 - 트럼프 반도체 관세…한국 기업 불똥?",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["팔로우십", "AI"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
url: "KE_MeQZgnPM",
|
||||
category: "리더십",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "정선근 교수가 알려주는 목디스크 지식",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
url: "IeF8r0ycgVg",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "꼰대가 되지 않고 건설적인 피드백을 하는 법",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
];
|
||||
|
||||
// 렌더러 초기화
|
||||
const renderer = new VideoCardRenderer({
|
||||
animationDelay: 50,
|
||||
});
|
||||
|
||||
// 슬라이더 초기화
|
||||
const slider = new VideoSlider({
|
||||
videos: videos,
|
||||
videosPerPage: 6,
|
||||
onPageChange: (pageVideos) => {
|
||||
renderer.renderCards(pageVideos);
|
||||
},
|
||||
});
|
||||
|
||||
// 비디오 모달 매니저 초기화
|
||||
const modalManager = new VideoModalManager({
|
||||
videos: videos,
|
||||
});
|
||||
|
||||
// 슬라이더 초기화 (첫 페이지 렌더링 포함)
|
||||
slider.init();
|
||||
renderer.renderCards(slider.getCurrentPageVideos());
|
||||
|
||||
// 모달 매니저 초기화 (카드 클릭 이벤트 등록)
|
||||
modalManager.init();
|
||||
|
||||
// ========================================
|
||||
// 게이지 차트 초기화
|
||||
// ========================================
|
||||
|
||||
const gauge = new GaugeChart({
|
||||
size: 832,
|
||||
strokeWidth: 31,
|
||||
maxValue: 50,
|
||||
padding: 20,
|
||||
outerTextOffset: 6,
|
||||
innerTextOffset: 35,
|
||||
dotRadius: 7,
|
||||
});
|
||||
|
||||
// 게이지 초기화
|
||||
gauge.init();
|
||||
|
||||
// 예시: 학습 시간 업데이트 (35분)
|
||||
gauge.update(25);
|
||||
|
||||
// ========================================
|
||||
// 키워드 모달 기능
|
||||
// ========================================
|
||||
|
||||
// DOM 요소
|
||||
const modal = document.getElementById("keywordModal");
|
||||
const settingsBtn = document.getElementById("keywordSettingsBtn");
|
||||
const checkIcon = modal.querySelector(".modal-header .btn-close");
|
||||
|
||||
// 현재 선택된 키워드 저장
|
||||
let allowKeywords = ["인물", "소통", "협업"]; // 초기값
|
||||
const maxAllowKeywords = 3;
|
||||
|
||||
// 모달 체크박스 상태 동기화
|
||||
function syncModalCheckboxes() {
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
checkbox.checked = allowKeywords.includes(keywordText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 체크박스 활성화/비활성화 상태 업데이트
|
||||
function updateCheckboxStates() {
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
const isMaxReached = allowKeywords.length >= maxAllowKeywords;
|
||||
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
const isSelected = allowKeywords.includes(keywordText);
|
||||
|
||||
if (!isSelected && isMaxReached) {
|
||||
checkbox.disabled = true;
|
||||
label.style.opacity = "0.5";
|
||||
label.style.cursor = "not-allowed";
|
||||
} else {
|
||||
checkbox.disabled = false;
|
||||
label.style.opacity = "1";
|
||||
label.style.cursor = "pointer";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 메인 화면 키워드 표시 업데이트
|
||||
function updateMainKeywordDisplay() {
|
||||
const allowArea = document.querySelector("#myKeyword");
|
||||
if (allowArea) {
|
||||
allowArea.innerHTML = "";
|
||||
|
||||
allowKeywords.forEach((keyword, index) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "kw-allow";
|
||||
label.setAttribute("for", `chk_allow_${index}`);
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = `chk_allow_${index}`;
|
||||
checkbox.checked = true;
|
||||
|
||||
const text = document.createTextNode(`#${keyword}`);
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(text);
|
||||
allowArea.appendChild(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 최대 개수 초과 메시지
|
||||
function showLimitMessage() {
|
||||
const modalHeader = modal.querySelector(".modal-header");
|
||||
if (modalHeader) {
|
||||
const existingMsg = modalHeader.querySelector(".limit-message");
|
||||
if (existingMsg) return;
|
||||
|
||||
const message = document.createElement("span");
|
||||
message.className = "limit-message";
|
||||
message.textContent = ` (최대 ${maxAllowKeywords}개까지 선택 가능)`;
|
||||
message.style.color = "#ff4444";
|
||||
message.style.fontSize = "14px";
|
||||
message.style.marginLeft = "10px";
|
||||
modalHeader.appendChild(message);
|
||||
|
||||
setTimeout(() => {
|
||||
message.remove();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 팝업 열기
|
||||
settingsBtn.onclick = () => {
|
||||
syncModalCheckboxes();
|
||||
updateCheckboxStates();
|
||||
modal.style.opacity = "";
|
||||
modal.style.display = "block";
|
||||
};
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = "none";
|
||||
updateMainKeywordDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
// 체크 아이콘 클릭 시 닫기
|
||||
if (checkIcon) {
|
||||
checkIcon.onclick = () => {
|
||||
modal.style.display = "none";
|
||||
updateMainKeywordDisplay();
|
||||
};
|
||||
}
|
||||
|
||||
// 모달 내 체크박스 이벤트
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
checkbox.onchange = () => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
|
||||
// 최대 개수 체크
|
||||
if (checkbox.checked && allowKeywords.length >= maxAllowKeywords) {
|
||||
checkbox.checked = false;
|
||||
showLimitMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkbox.disabled) {
|
||||
// 키워드 추가/제거
|
||||
if (checkbox.checked) {
|
||||
if (!allowKeywords.includes(keywordText)) {
|
||||
allowKeywords.push(keywordText);
|
||||
}
|
||||
} else {
|
||||
allowKeywords = allowKeywords.filter((k) => k !== keywordText);
|
||||
}
|
||||
updateCheckboxStates();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}); // DOMContentLoaded 종료
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!-- keyword modal -->
|
||||
<div class="modal keyword" id="keywordModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button class="btn-close">
|
||||
나의 키워드 수정<i class="ico-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<ul class="keyword-tag">
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_1">
|
||||
<input type="checkbox" id="kwtag_1" /> #온보딩
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_2">
|
||||
<input type="checkbox" id="kwtag_2" /> #성장
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_3">
|
||||
<input type="checkbox" id="kwtag_3" /> #코칭
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_4">
|
||||
<input type="checkbox" id="kwtag_4" checked /> #인물
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_5">
|
||||
<input type="checkbox" id="kwtag_5" checked /> #소통
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_6">
|
||||
<input type="checkbox" id="kwtag_6" checked /> #협업
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_7">
|
||||
<input type="checkbox" id="kwtag_7" /> #AI
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_8">
|
||||
<input type="checkbox" id="kwtag_8" /> #IT테크
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_9">
|
||||
<input type="checkbox" id="kwtag_9" />
|
||||
#중간관리자
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_10">
|
||||
<input type="checkbox" id="kwtag_10" /> #리더십
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_11">
|
||||
<input type="checkbox" id="kwtag_11" /> #팔로우십
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_12">
|
||||
<input type="checkbox" id="kwtag_12" /> #동기부여
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_13">
|
||||
<input type="checkbox" id="kwtag_13" /> #인간관계
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_14">
|
||||
<input type="checkbox" id="kwtag_14" /> #스킬업
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_15">
|
||||
<input type="checkbox" id="kwtag_15" /> #피드백
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_16">
|
||||
<input type="checkbox" id="kwtag_16" /> #커리어
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_17">
|
||||
<input type="checkbox" id="kwtag_17" /> #경영
|
||||
</label>
|
||||
</li>
|
||||
<li class="kw-box">
|
||||
<label for="kwtag_18">
|
||||
<input type="checkbox" id="kwtag_18" /> #경제
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // keyword modal -->
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
$navPath = basename(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '');
|
||||
parse_str(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?: '', $navQuery);
|
||||
$navCate = $navQuery['cate'] ?? '';
|
||||
|
||||
/** depth01: 현재 스크립트가 해당 php와 같으면 부모 li active (서브메뉴 페이지 포함) */
|
||||
function nav_active_depth01($phpFile) {
|
||||
global $navPath;
|
||||
return $navPath === $phpFile ? ' class="active"' : '';
|
||||
}
|
||||
|
||||
/** depth02: 스크립트 + cate 쿼리까지 일치 */
|
||||
function nav_active_depth02($phpFile, $cate) {
|
||||
global $navPath, $navCate;
|
||||
return ($navPath === $phpFile && $navCate === $cate) ? ' class="active"' : '';
|
||||
}
|
||||
?>
|
||||
<div class="nav-group">
|
||||
<ul class="depth01">
|
||||
<li<?php echo nav_active_depth01('myclass.php'); echo nav_active_depth01('myclass_list.php');?>>
|
||||
<a href="/edu/skin/myclass.php"> 마이클래스 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit">0<small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: 0%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('onboarding.php'); ?>>
|
||||
<a href="/edu/skin/onboarding.php" data-nav="onboarding"> 온보딩 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit" id="onboardingNavPercent" style="left: calc(0% - 3px);">0<small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" id="onboardingNavBar" style="width: 0%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('learning.php'); ?>>
|
||||
<a href="/edu/skin/learning.php"> 법정교육 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit">0<small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: 0%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('leadership.php'); ?>>
|
||||
<a href="/edu/skin/leadership.php">리더십</a>
|
||||
<ul class="depth02">
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L01'); ?>>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L01" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_01.svg" />
|
||||
</span>
|
||||
리더십 시작하기
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L02'); ?>>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L02" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_02.svg" />
|
||||
</span>
|
||||
셀프 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L03'); ?>>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L03" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_03.svg" />
|
||||
</span>
|
||||
팀 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L04'); ?>>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L04" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_04.svg" />
|
||||
</span>
|
||||
실전조직 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L05'); ?>>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L05" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_05.svg" />
|
||||
</span>
|
||||
리더케이스탐구
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('insight.php'); ?>>
|
||||
<a href="/edu/skin/insight.php">인사이트 </a>
|
||||
<!-- 2depth -->
|
||||
<ul class="depth02">
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I01'); ?>>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I01" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_insight_01.svg" />
|
||||
</span>
|
||||
경제와 사회
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I02'); ?>>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I02" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_insight_02.svg" />
|
||||
</span>
|
||||
기술과 미래
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I03'); ?>>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I03" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_insight_03.svg" />
|
||||
</span>
|
||||
스킬 업
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I04'); ?>>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I04" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_insight_04.svg" />
|
||||
</span>
|
||||
행복과 건강
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I05'); ?>>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I05" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_insight_05.svg" />
|
||||
</span>
|
||||
피플스토리
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I06'); ?>>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I06" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_insight_06.svg" />
|
||||
</span>
|
||||
라이프
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<?php echo nav_active_depth01('biztrend.php'); ?>>
|
||||
<a href="/edu/skin/biztrend.php">비즈트렌드</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!function_exists('edu_start_session')) {
|
||||
function edu_start_session(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('edu_current_member_id')) {
|
||||
function edu_current_member_id(): string
|
||||
{
|
||||
edu_start_session();
|
||||
$memberId = trim((string)($_SESSION['ss_mb_id'] ?? $_SESSION['member_id'] ?? ''));
|
||||
return $memberId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('edu_is_logged_in')) {
|
||||
function edu_is_logged_in(): bool
|
||||
{
|
||||
return edu_current_member_id() !== '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('edu_require_login')) {
|
||||
function edu_require_login(): void
|
||||
{
|
||||
if (edu_is_logged_in()) {
|
||||
return;
|
||||
}
|
||||
|
||||
header('Location: http://erp.baroncs.co.kr/mobile/sys/controller/Study_controller.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta http-equiv="X-UA-compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<!-- <meta http-equiv="pragma" content="no-cache"> -->
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/svg+xml"
|
||||
href="/img/favicon.ico"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100..900&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/lib/swiper11.min.css" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
||||
|
||||
<script src="https://www.youtube.com/iframe_api"></script>
|
||||
<script src="/js/lib/swiper11.min.js"></script>
|
||||
<script src="/js/lib/lottie.min.js"></script>
|
||||
<script src="/js/lib/gsap.min.js"></script>
|
||||
<script src="/js/lib/scrolltrigger.min.js"></script>
|
||||
<!--
|
||||
공통 모듈 로딩 순서 (반드시 유지)
|
||||
1. Utils → 범용 유틸(딜레이, 디바운스, 날짜포맷 등)
|
||||
2. DOMUtils → DOM 조작(선택, 페이드, 이벤트 위임)
|
||||
3. ErrorHandler → 에러 처리 (다른 모듈에서 사용)
|
||||
4. EventManager → 이벤트 중앙 관리 (선택적, 없으면 DOMUtils.delegate 폴백)
|
||||
5. AnimationUtils 이하 → 애니메이션, 비디오, 모달 등
|
||||
-->
|
||||
<script src="/js/lib/jquery-3.6.1.min.js"></script>
|
||||
<script src="/js/common/Utils.js" defer></script>
|
||||
<script src="/js/common/DOMUtils.js" defer></script>
|
||||
<script src="/js/common/ErrorHandler.js" defer></script>
|
||||
<script src="/js/common/EventManager.js" defer></script>
|
||||
<script src="/js/common/AnimationUtils.js" defer></script>
|
||||
<script src="/js/common/VideoBase.js" defer></script>
|
||||
<script src="/js/common/ModalBase.js" defer></script>
|
||||
<script src="/js/common/ModalUtils.js" defer></script>
|
||||
<script src="/js/common/VideoModalBase.js" defer></script>
|
||||
<script src="/js/common/LearningGuideModal.js" defer></script>
|
||||
<script src="/js/common/ConfigManager.js" defer></script>
|
||||
<script src="/js/common/GaugeBase.js" defer></script>
|
||||
|
||||
<!-- 공통 스크립트 (모든 공통 모듈 로드 후 실행) -->
|
||||
<script src="/js/common.js" defer></script>
|
||||
<title>배움터</title>
|
||||
@@ -0,0 +1,589 @@
|
||||
<!-- header -->
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$headerSearchQuery = trim((string)($_GET['q'] ?? ''));
|
||||
|
||||
$memberId = (string)($_SESSION['member_id'] ?? '');
|
||||
$isLoggedIn = $memberId !== '';
|
||||
$memberName = isset($_SESSION['member_name']) ? trim((string)$_SESSION['member_name']) : '';
|
||||
$authLevel = strtoupper(trim((string)($_SESSION['auth_level'] ?? '')));
|
||||
$canSeePrivilegedButtons = $isLoggedIn && in_array($authLevel, ['LE10001', 'LE10002'], true);
|
||||
$adminLandingUrl = ($authLevel === 'LE10002')
|
||||
? '/admin/skin/legal_edu.php'
|
||||
: '/admin/skin/index.php';
|
||||
|
||||
// ── 알림 데이터 조회 (edu_notifications) ──
|
||||
$notifications = [];
|
||||
$hasNewAlert = false;
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 헤더에서 바로 사용할 기본 변수
|
||||
// ---------------------------------------------------------
|
||||
$header_myclass_percent = 0;
|
||||
$header_onboarding_percent = 0;
|
||||
$header_legal_percent = 0;
|
||||
|
||||
|
||||
|
||||
require_once __DIR__ . '/../../bbs/init_data_for_header.php';
|
||||
|
||||
if($memberId=="M21420" ){
|
||||
/*
|
||||
echo "memberId=".$memberId."<br>";
|
||||
echo "header_myclass_percent=".$header_myclass_percent."<br>";
|
||||
echo "header_onboarding_percent=".$header_onboarding_percent."<br>";
|
||||
echo "header_legal_percent=".$header_legal_percent."<br>";
|
||||
*/
|
||||
//exit;
|
||||
}
|
||||
|
||||
if ($isLoggedIn) {
|
||||
try {
|
||||
require_once __DIR__ . '/../../bbs/db_conn.php';
|
||||
$pdoHeader = db_conn();
|
||||
|
||||
$corpCode = (string)($_SESSION['sys_comp_code'] ?? '');
|
||||
|
||||
$stmtNotif = $pdoHeader->prepare("
|
||||
SELECT n.seq,
|
||||
fn_get_code_name(n.type_code) AS type_name,
|
||||
n.type_code,
|
||||
n.message,
|
||||
n.sent_at,
|
||||
n.end_date
|
||||
FROM edu_notifications n
|
||||
WHERE n.member_id = ?
|
||||
AND n.corp_code = ?
|
||||
AND n.end_date >= CURDATE()
|
||||
ORDER BY n.sent_at DESC
|
||||
");
|
||||
$stmtNotif->execute([$memberId, $corpCode]);
|
||||
$notifications = $stmtNotif->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmtAuth = $pdoHeader->prepare(
|
||||
"SELECT auth_level
|
||||
FROM edu_users
|
||||
WHERE member_id = ?
|
||||
ORDER BY (sys_comp_code = ?) DESC, sys_comp_code ASC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmtAuth->execute([$memberId, $corpCode]);
|
||||
$authLevelFromDb = $stmtAuth->fetchColumn();
|
||||
$authLevel = strtoupper(trim((string)($authLevelFromDb ?? '')));
|
||||
$canSeePrivilegedButtons = $isLoggedIn && in_array($authLevel, ['LE10001', 'LE10002'], true);
|
||||
$adminLandingUrl = ($authLevel === 'LE10002')
|
||||
? '/admin/skin/legal_edu.php'
|
||||
: '/admin/skin/index.php';
|
||||
|
||||
// sent_at 이 오늘인 알림이 있으면 on 표시
|
||||
$today = date('Y-m-d');
|
||||
foreach ($notifications as $notif) {
|
||||
if (!empty($notif['sent_at']) && substr($notif['sent_at'], 0, 10) === $today) {
|
||||
$hasNewAlert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if($memberId=="M21420" ){/*
|
||||
echo "memberId=".$memberId."<br>";
|
||||
echo "header_myclass_percent=".$header_myclass_percent."<br>";
|
||||
echo "header_onboarding_percent=".$header_onboarding_percent."<br>";
|
||||
echo "header_legal_percent=".$header_legal_percent."<br>";
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
// DB 오류 시 빈 배열 유지
|
||||
$notifications = [];
|
||||
$hasNewAlert = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($learningGuideDefaultTab)) {
|
||||
$guideScript = strtolower(basename($_SERVER['SCRIPT_NAME'] ?? '', '.php'));
|
||||
$guideUri = strtolower(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '');
|
||||
|
||||
$guidePageMap = [
|
||||
'index' => 'main',
|
||||
'main' => 'main',
|
||||
'myclass' => 'myclass',
|
||||
'myclass_list' => 'myclass',
|
||||
'onboarding' => 'onboarding',
|
||||
'learning' => 'legal',
|
||||
'legal_edu' => 'legal',
|
||||
'legal' => 'legal',
|
||||
'leadership' => 'leadership',
|
||||
'insight' => 'insight',
|
||||
'biztrend' => 'biztrend',
|
||||
'mypage' => 'mypage',
|
||||
'player' => 'player',
|
||||
];
|
||||
|
||||
if (isset($guidePageMap[$guideScript])) {
|
||||
$learningGuideDefaultTab = $guidePageMap[$guideScript];
|
||||
} else {
|
||||
foreach ($guidePageMap as $needle => $key) {
|
||||
if ($needle !== 'player' && (strpos($guideUri, '/' . $needle) !== false || strpos($guideUri, $needle . '.php') !== false)) {
|
||||
$learningGuideDefaultTab = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$learningGuideDefaultTab = $learningGuideDefaultTab ?? 'player';
|
||||
?>
|
||||
|
||||
<!--
|
||||
$header_myclass_percent
|
||||
$header_onboarding_percent
|
||||
$header_legal_percent
|
||||
-->
|
||||
<div class="nav-wrap">
|
||||
<div class="inner">
|
||||
<!-- header -->
|
||||
<header class="header">
|
||||
<a href="./index.php"><h1>배움터.</h1></a>
|
||||
<button class="btn-guide" type="button" title="가이드 열기/닫기" data-default-guide-key="<?= htmlspecialchars($learningGuideDefaultTab, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<div class="btn-guide-inner">
|
||||
<span class="ico-guide-wrap"><i class="ico-guide"></i></span>
|
||||
<span class="guide-text"><span class="guide-type">학습</span> 가이드</span>
|
||||
</div>
|
||||
</button>
|
||||
</header>
|
||||
<!--// header -->
|
||||
<nav class="nav"><?php include(__DIR__ . "/_nav.php") ?></nav>
|
||||
<div class="item-area">
|
||||
<!-- 모바일에서만 보이는 검색 아이콘 버튼 -->
|
||||
<div class="item-box">
|
||||
<button class="btn-search" type="button" title="검색 열기"></button>
|
||||
</div>
|
||||
<!-- 데스크톱 기본 검색 인풋 -->
|
||||
<form class="input-group" action="./search_result.php" method="get" autocomplete="off">
|
||||
<input type="text" name="q" value="<?= htmlspecialchars($headerSearchQuery, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" placeholder="검색" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" />
|
||||
<button type="submit" class="ico-search" aria-label="검색"></button>
|
||||
<button class="btn-clear" type="button"></button>
|
||||
<div class="search-suggestions">
|
||||
<ul class="search-suggestions-list" data-search-history-list="desktop"></ul>
|
||||
</div>
|
||||
</form>
|
||||
<div class="item-box">
|
||||
<button class="alerts<?= $hasNewAlert ? ' on' : '' ?>" title="알림"></button>
|
||||
<div class="alerts-area">
|
||||
<ul class="alerts-list">
|
||||
<?php if (empty($notifications)): ?>
|
||||
<li class="alert-item">
|
||||
<p class="alert-desc">알림이 없습니다.</p>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<?php foreach ($notifications as $notif): ?>
|
||||
<li class="alert-item">
|
||||
<strong class="alert-title"><?= htmlspecialchars($notif['type_name'] ?? '', ENT_QUOTES, 'UTF-8') ?></strong>
|
||||
<p class="alert-desc" title="<?= htmlspecialchars($notif['message'] ?? '', ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($notif['message'] ?? '', ENT_QUOTES, 'UTF-8') ?></p>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-box">
|
||||
<a class="mypage<?php echo nav_active_depth01('mypage.php') ? ' active' : ''; ?>" title="마이페이지" href="/skin/mypage.php"></a>
|
||||
</div>
|
||||
|
||||
<!-- 로그인/로그아웃 버튼 -->
|
||||
<div class="item-box">
|
||||
<?php if ($isLoggedIn): ?>
|
||||
<?php if ($canSeePrivilegedButtons): ?>
|
||||
<button class="btn-auth btn-logout" type="button" title="로그아웃" id="btnAuthDesktop"></button>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<a class="btn-auth btn-login" href="/bbs/login.php" title="로그인"></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canSeePrivilegedButtons): ?>
|
||||
<div class="item-box">
|
||||
<a class="btn-admin" title="관리자 페이지" href="<?= htmlspecialchars($adminLandingUrl, ENT_QUOTES, 'UTF-8') ?>" target="_blank"></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="item-box">
|
||||
<button class="btn-sitemap" type="button" title="메뉴 열기/닫기" aria-expanded="false" aria-controls="siteMap"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="site-map" id="siteMap" aria-hidden="true">
|
||||
<div class="site-map-backdrop" aria-hidden="true"></div>
|
||||
<div class="site-map-inner">
|
||||
|
||||
<div class="site-map-header">
|
||||
<a href="./index.php" class="mo-logo">배움터</a>
|
||||
<div class="item-area">
|
||||
<div class="item-box">
|
||||
<a class="mypage" title="마이페이지" href="./mypage.php"></a>
|
||||
</div>
|
||||
<!-- 로그인/로그아웃 버튼 (모바일) -->
|
||||
<div class="item-box">
|
||||
<?php if ($isLoggedIn): ?>
|
||||
<?php if ($canSeePrivilegedButtons): ?>
|
||||
<button class="btn-auth btn-logout" type="button" title="로그아웃" id="btnAuthMobile"></button>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<a class="btn-auth btn-login" href="/bbs/login.php" title="로그인"></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="button" class="btn-close" aria-label="사이트맵 닫기"></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="site-map-body">
|
||||
|
||||
<!-- 퀵 메뉴 -->
|
||||
<div class="site-map-quick">
|
||||
<a href="./myclass.php" class="quick-item">
|
||||
<span class="quick-icon">
|
||||
<img src="/img/img_sitemap_01.svg" alt="" />
|
||||
</span>
|
||||
<span class="quick-title">마이클래스</span>
|
||||
<div class="state-box">
|
||||
<span class="unit"><?=$header_myclass_percent?><small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: <?=$header_myclass_percent?>%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="./onboarding.php" class="quick-item" data-quick="onboarding">
|
||||
<span class="quick-icon">
|
||||
<img src="/img/img_sitemap_02.svg" alt="" />
|
||||
</span>
|
||||
<span class="quick-title">온보딩</span>
|
||||
<div class="state-box">
|
||||
<span class="unit" id="onboardingQuickPercent" style="left: 0%;"><?=$header_onboarding_percent?><small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" id="onboardingQuickBar" style="width: <?=$header_onboarding_percent?>%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="./learning.php" class="quick-item">
|
||||
<span class="quick-icon">
|
||||
<img src="/img/img_sitemap_03.svg" alt="" />
|
||||
</span>
|
||||
<span class="quick-title">법정교육</span>
|
||||
<div class="state-box">
|
||||
<span class="unit" style="left:100%;"><?=$header_legal_percent?><small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: <?=$header_legal_percent?>%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 아코디언 메뉴 -->
|
||||
<ul class="site-map-menu">
|
||||
<li class="menu-section<?php echo nav_active_depth01('leadership.php') ? ' active' : ''; ?>">
|
||||
<button type="button" class="menu-section-title">
|
||||
리더십<i class="ico-chevron"></i>
|
||||
</button>
|
||||
<ul class="menu-section-list">
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L01'); ?>><a href="./leadership.php?cate=CA200L01">리더십 시작하기</a></li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L02'); ?>><a href="./leadership.php?cate=CA200L02">셀프 리더십</a></li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L03'); ?>><a href="./leadership.php?cate=CA200L03">팀 리더십</a></li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L04'); ?>><a href="./leadership.php?cate=CA200L04">실전조직 리더십</a></li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L05'); ?>><a href="./leadership.php?cate=CA200L05">리더케이스탐구</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="menu-section<?php echo nav_active_depth01('insight.php') ? ' active' : ''; ?>">
|
||||
<button type="button" class="menu-section-title">
|
||||
인사이트
|
||||
<i class="ico-chevron"></i>
|
||||
</button>
|
||||
<ul class="menu-section-list">
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I01'); ?>><a href="./insight.php?cate=CA200I01">경제와 사회</a></li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I02'); ?>><a href="./insight.php?cate=CA200I02">기술과 미래</a></li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I03'); ?>><a href="./insight.php?cate=CA200I03">스킬 업</a></li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I04'); ?>><a href="./insight.php?cate=CA200I04">행복과 건강</a></li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I05'); ?>><a href="./insight.php?cate=CA200I05">피플스토리</a></li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I06'); ?>><a href="./insight.php?cate=CA200I06">라이프</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="menu-section<?php echo nav_active_depth01('biztrend.php') ? ' active' : ''; ?>">
|
||||
<a href="./biztrend.php" class="menu-section-title">
|
||||
비즈트렌드
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 검색 오버레이 (Figma 디자인: 오른쪽 슬라이드 패널) -->
|
||||
<div class="mo-search-layer" aria-hidden="true">
|
||||
<div class="mo-search-backdrop" aria-hidden="true"></div>
|
||||
<div class="mo-search-inner">
|
||||
<div class="mo-search-header">
|
||||
<a href="./index.html" class="mo-search-logo">배움터</a>
|
||||
<button type="button" class="btn-close-search" aria-label="검색 닫기"></button>
|
||||
</div>
|
||||
<div class="mo-search-body">
|
||||
<div class="mo-search-input">
|
||||
<form class="input-group" action="./search_result.php" method="get" autocomplete="off">
|
||||
<input type="text" name="q" value="<?= htmlspecialchars($headerSearchQuery, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" placeholder="검색어를 입력해주세요" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" />
|
||||
<button class="btn-clear" type="button" aria-label="입력 지우기"></button>
|
||||
<button type="submit" class="ico-search" aria-label="검색"></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mo-search-history">
|
||||
<ul class="search-suggestions-list" data-search-history-list="mobile"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php // 페이지별 기본 탭: header include 전에 $learningGuideDefaultTab = 'legal'; 등으로 설정 (자동 감지보다 우선) ?>
|
||||
<?php include(__DIR__ . "/../_modal/learning-guide.php") ?>
|
||||
|
||||
<!--// header -->
|
||||
<script>
|
||||
// 로그아웃 버튼 이벤트 처리 (모든 페이지 공통)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function handleLogout() {
|
||||
if (!confirm('로그아웃 하시겠습니까?')) return;
|
||||
|
||||
console.log('[Logout] Starting logout process...');
|
||||
|
||||
fetch('/bbs/logout.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
.then(function(response) {
|
||||
console.log('[Logout] Response status:', response.status);
|
||||
if (!response.ok) throw new Error('HTTP error! status: ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log('[Logout] Response data:', data);
|
||||
if (data.success) {
|
||||
// window.location.href = '/skin/index.php';
|
||||
window.location.href = '/skin/login.php';
|
||||
} else {
|
||||
alert(data.message || '로그아웃 실패');
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('[Logout] Error:', error);
|
||||
alert('로그아웃 중 오류가 발생했습니다: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
var btnLogoutDesktop = document.getElementById('btnAuthDesktop');
|
||||
var btnLogoutMobile = document.getElementById('btnAuthMobile');
|
||||
if (btnLogoutDesktop) btnLogoutDesktop.addEventListener('click', function(e){ e.preventDefault(); handleLogout(); });
|
||||
if (btnLogoutMobile) btnLogoutMobile.addEventListener('click', function(e){ e.preventDefault(); handleLogout(); });
|
||||
|
||||
console.log('[Header] Auth buttons setup done. desktop:', !!btnLogoutDesktop, 'mobile:', !!btnLogoutMobile);
|
||||
});
|
||||
|
||||
// 검색어 동기화 (search_result.php 전용)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
if (!/\/search_result\.php(?:$|\?)/.test(window.location.pathname + window.location.search)) return;
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var q = (params.get('q') || '').trim();
|
||||
if (!q) return;
|
||||
|
||||
document.querySelectorAll('.input-group input[name="q"]').forEach(function (input) {
|
||||
input.value = q;
|
||||
});
|
||||
|
||||
var keywordEl = document.querySelector('.search-keyword');
|
||||
if (keywordEl) keywordEl.textContent = q;
|
||||
} catch (e) {
|
||||
console.warn('[header search sync]', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 최근 검색어 렌더링 (헤더 공통)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var apiUrl = '/bbs/api/search_logs.php?limit=20';
|
||||
var saveApiUrl = '/bbs/api/search_logs.php';
|
||||
var searchResultBaseUrl = '/skin/search_result.php?q=';
|
||||
|
||||
var desktopInput = document.querySelector('.item-area > form.input-group input[name="q"]');
|
||||
var mobileInput = document.querySelector('.mo-search-layer .input-group input[name="q"]');
|
||||
|
||||
var desktopList = document.querySelector('[data-search-history-list="desktop"]');
|
||||
var mobileList = document.querySelector('[data-search-history-list="mobile"]');
|
||||
var searchForms = document.querySelectorAll('form.input-group[action="./search_result.php"]');
|
||||
|
||||
var cacheLogs = null;
|
||||
var cacheAt = 0;
|
||||
var isLoading = false;
|
||||
var lastSavedKeyword = '';
|
||||
var lastSavedAt = 0;
|
||||
|
||||
function clearList(listEl) {
|
||||
while (listEl && listEl.firstChild) {
|
||||
listEl.removeChild(listEl.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function appendEmptyRow(listEl) {
|
||||
if (!listEl) return;
|
||||
var li = document.createElement('li');
|
||||
var span = document.createElement('span');
|
||||
span.textContent = '최근 검색어가 없습니다.';
|
||||
li.appendChild(span);
|
||||
listEl.appendChild(li);
|
||||
}
|
||||
|
||||
function appendKeywordRow(listEl, keyword) {
|
||||
if (!listEl || !keyword) return;
|
||||
|
||||
var li = document.createElement('li');
|
||||
var link = document.createElement('a');
|
||||
var icon = document.createElement('i');
|
||||
var text = document.createElement('span');
|
||||
|
||||
link.href = searchResultBaseUrl + encodeURIComponent(keyword);
|
||||
icon.className = 'ico-history';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
text.textContent = keyword;
|
||||
|
||||
link.appendChild(icon);
|
||||
link.appendChild(text);
|
||||
li.appendChild(link);
|
||||
listEl.appendChild(li);
|
||||
}
|
||||
|
||||
function renderLogs(logs) {
|
||||
var listTargets = [desktopList, mobileList];
|
||||
|
||||
listTargets.forEach(function (listEl) {
|
||||
clearList(listEl);
|
||||
|
||||
if (!Array.isArray(logs) || logs.length === 0) {
|
||||
appendEmptyRow(listEl);
|
||||
return;
|
||||
}
|
||||
|
||||
logs.forEach(function (log) {
|
||||
var keyword = String((log && log.keyword) || '').trim();
|
||||
if (!keyword) return;
|
||||
appendKeywordRow(listEl, keyword);
|
||||
});
|
||||
|
||||
if (!listEl.children.length) {
|
||||
appendEmptyRow(listEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadRecentLogs(force) {
|
||||
var now = Date.now();
|
||||
var isCacheFresh = cacheLogs !== null && (now - cacheAt) < 30000;
|
||||
if (!force && isCacheFresh) {
|
||||
renderLogs(cacheLogs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
fetch(apiUrl, { credentials: 'same-origin' })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
var logs = (json && json.data && Array.isArray(json.data.logs)) ? json.data.logs : [];
|
||||
cacheLogs = logs;
|
||||
cacheAt = Date.now();
|
||||
renderLogs(logs);
|
||||
})
|
||||
.catch(function () {
|
||||
renderLogs([]);
|
||||
})
|
||||
.finally(function () {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function postSearchLog(keyword) {
|
||||
var payload = new URLSearchParams();
|
||||
payload.append('keyword', keyword);
|
||||
|
||||
try {
|
||||
if (navigator.sendBeacon) {
|
||||
return navigator.sendBeacon(saveApiUrl, payload);
|
||||
}
|
||||
} catch (e) {
|
||||
// sendBeacon 실패 시 keepalive fetch로 폴백
|
||||
}
|
||||
|
||||
fetch(saveApiUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: payload.toString(),
|
||||
keepalive: true
|
||||
}).catch(function () {
|
||||
// 저장 실패 시 검색 이동은 막지 않는다.
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[desktopInput, mobileInput].forEach(function (inputEl) {
|
||||
if (!inputEl) return;
|
||||
|
||||
inputEl.addEventListener('focus', function () {
|
||||
loadRecentLogs(false);
|
||||
});
|
||||
|
||||
inputEl.addEventListener('click', function () {
|
||||
loadRecentLogs(false);
|
||||
});
|
||||
});
|
||||
|
||||
searchForms.forEach(function (formEl) {
|
||||
formEl.addEventListener('submit', function (e) {
|
||||
var inputEl = formEl.querySelector('input[name="q"]');
|
||||
var keyword = String((inputEl && inputEl.value) || '').trim();
|
||||
if (!keyword) {
|
||||
e.preventDefault();
|
||||
if (inputEl) inputEl.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (formEl.dataset.searchLogSubmitting === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var normalized = keyword.toLowerCase();
|
||||
if (lastSavedKeyword === normalized && (now - lastSavedAt) < 1500) {
|
||||
return;
|
||||
}
|
||||
|
||||
formEl.dataset.searchLogSubmitting = '1';
|
||||
lastSavedKeyword = normalized;
|
||||
lastSavedAt = now;
|
||||
postSearchLog(keyword);
|
||||
|
||||
// 짧은 시간 후 플래그 해제: 연속 검색 시 다음 submit 허용
|
||||
setTimeout(function () {
|
||||
delete formEl.dataset.searchLogSubmitting;
|
||||
}, 1200);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
$navPath = basename(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '');
|
||||
parse_str(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?: '', $navQuery);
|
||||
$navCate = $navQuery['cate'] ?? '';
|
||||
|
||||
/** depth01: 현재 스크립트가 해당 php와 같으면 부모 li active (서브메뉴 페이지 포함) */
|
||||
function nav_active_depth01($phpFile) {
|
||||
global $navPath;
|
||||
return $navPath === $phpFile ? ' class="active"' : '';
|
||||
}
|
||||
|
||||
/** depth02: 스크립트 + cate 쿼리까지 일치 */
|
||||
function nav_active_depth02($phpFile, $cate) {
|
||||
global $navPath, $navCate;
|
||||
return ($navPath === $phpFile && $navCate === $cate) ? ' class="active"' : '';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="nav-group">
|
||||
<ul class="depth01">
|
||||
<li<?php echo nav_active_depth01('myclass.php'); echo nav_active_depth01('myclass_list.php');?>>
|
||||
<a href="/skin/myclass.php"> 마이클래스 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit"><?=$header_myclass_percent?><small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width:<?=$header_myclass_percent?>%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('onboarding.php'); ?>>
|
||||
<a href="/skin/onboarding.php" data-nav="onboarding"> 온보딩 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit" id="onboardingNavPercent" style="left: calc(0% - 3px);"><?=$header_onboarding_percent?><small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" id="onboardingNavBar" style="width: <?=$header_onboarding_percent?>%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('learning.php'); ?>>
|
||||
<a href="/skin/learning.php"> 법정교육 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit"><?=$header_legal_percent?><small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: <?=$header_legal_percent?>%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('leadership.php'); ?>>
|
||||
<a href="/skin/leadership.php">리더십</a>
|
||||
<ul class="depth02">
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L01'); ?>>
|
||||
<a href="/skin/leadership.php?cate=CA200L01" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_leadership_01.svg" />
|
||||
</span>
|
||||
리더십 시작하기
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L02'); ?>>
|
||||
<a href="/skin/leadership.php?cate=CA200L02" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_leadership_02.svg" />
|
||||
</span>
|
||||
셀프 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L03'); ?>>
|
||||
<a href="/skin/leadership.php?cate=CA200L03" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_leadership_03.svg" />
|
||||
</span>
|
||||
팀 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L04'); ?>>
|
||||
<a href="/skin/leadership.php?cate=CA200L04" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_leadership_04.svg" />
|
||||
</span>
|
||||
실전조직 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('leadership.php', 'CA200L05'); ?>>
|
||||
<a href="/skin/leadership.php?cate=CA200L05" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_leadership_05.svg" />
|
||||
</span>
|
||||
리더케이스탐구
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth01('insight.php'); ?>>
|
||||
<a href="/skin/insight.php">인사이트 </a>
|
||||
<!-- 2depth -->
|
||||
<ul class="depth02">
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I01'); ?>>
|
||||
<a href="/skin/insight.php?cate=CA200I01" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_insight_01.svg" />
|
||||
</span>
|
||||
경제와 사회
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I02'); ?>>
|
||||
<a href="/skin/insight.php?cate=CA200I02" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_insight_02.svg" />
|
||||
</span>
|
||||
기술과 미래
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I03'); ?>>
|
||||
<a href="/skin/insight.php?cate=CA200I03" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_insight_03.svg" />
|
||||
</span>
|
||||
스킬 업
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I04'); ?>>
|
||||
<a href="/skin/insight.php?cate=CA200I04" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_insight_04.svg" />
|
||||
</span>
|
||||
행복과 건강
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I05'); ?>>
|
||||
<a href="/skin/insight.php?cate=CA200I05" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_insight_05.svg" />
|
||||
</span>
|
||||
피플스토리
|
||||
</a>
|
||||
</li>
|
||||
<li<?php echo nav_active_depth02('insight.php', 'CA200I06'); ?>>
|
||||
<a href="/skin/insight.php?cate=CA200I06" >
|
||||
<span class="ico-box">
|
||||
<img src="/img/ico/ico_insight_06.svg" />
|
||||
</span>
|
||||
라이프
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<?php echo nav_active_depth01('biztrend.php'); ?>>
|
||||
<a href="/skin/biztrend.php">비즈트렌드</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,139 @@
|
||||
<div class="nav-group">
|
||||
<ul class="depth01">
|
||||
<li>
|
||||
<a href="/edu/skin/myclass.php"> 마이클래스 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit">0<small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: 0%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/onboarding.php" data-nav="onboarding"> 온보딩 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit" id="onboardingNavPercent" style="left: calc(0% - 3px);">0<small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" id="onboardingNavBar" style="width: 0%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/learning.php"> 법정교육 </a>
|
||||
<div class="state-box">
|
||||
<span class="unit">0<small>%</small></span>
|
||||
<span class="progress">
|
||||
<span class="progress-bar" style="width: 0%"></span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/leadership.php">리더십</a>
|
||||
<ul class="depth02">
|
||||
<li>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L01" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_01.svg" />
|
||||
</span>
|
||||
리더십 입문
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L02" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_02.svg" />
|
||||
</span>
|
||||
셀프 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L03" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_03.svg" />
|
||||
</span>
|
||||
팀 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L04" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_04.svg" />
|
||||
</span>
|
||||
실전 리더십
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/leadership.php?cate=CA200L05" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_leadership_05.svg" />
|
||||
</span>
|
||||
인물탐구
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php">인사이트 </a>
|
||||
<!-- 2depth -->
|
||||
<ul class="depth02">
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I01" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_economy.svg" />
|
||||
</span>
|
||||
경제와 사회
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I02" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_future.svg" />
|
||||
</span>
|
||||
기술과 미래
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I03" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_trend.svg" />
|
||||
</span>
|
||||
트렌드 업
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I04" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_health.svg" />
|
||||
</span>
|
||||
행복과 건강
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I05" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_people.svg" />
|
||||
</span>
|
||||
피플스토리
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/edu/skin/insight.php?cate=CA200I06" >
|
||||
<span class="ico-box">
|
||||
<img src="/edu/img/ico/ico_life.svg" />
|
||||
</span>
|
||||
라이프
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/edu/skin/biztrend.php">비즈트렌드</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<!-- 목표 상세 팝업 레이어 (myclass, myclass_04 공용) -->
|
||||
<div class="goal-layer hidden" id="goalLayer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="학습목표 상세">
|
||||
|
||||
<div class="goal-layer-content">
|
||||
<div class="goal-layer-header">
|
||||
<button class="btn-close-layer" type="button" id="btnCloseLayer" aria-label="닫기">
|
||||
<span class="blind">닫기</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="goal-layer-main">
|
||||
<div class="goal-popup" role="document">
|
||||
<div class="goal-main-slider">
|
||||
<div class="swiper goal-popup-swiper">
|
||||
<div class="swiper-wrapper" id="goalSlideWrapper">
|
||||
<!-- 슬라이드 1 (템플릿, JS에서 2~8번 복제) -->
|
||||
<div class="swiper-slide goal-slide" data-goal-id="1">
|
||||
<!-- 미니 카드: 비활성일 때만 표시 -->
|
||||
<div class="slide-mini-card">
|
||||
<div class="goal-slide-icon-mini" aria-hidden="true"></div>
|
||||
<strong class="goal-slide-title-mini"></strong>
|
||||
<div class="slide-mini-divider" aria-hidden="true"></div>
|
||||
<p class="goal-slide-desc-mini"></p>
|
||||
</div>
|
||||
<!-- 풀 콘텐츠: 활성일 때만 표시 -->
|
||||
<div class="popup-scroll">
|
||||
<div class="popup-body">
|
||||
<div class="popup-top">
|
||||
<div class="title-box">
|
||||
<div class="title-icon goal-slide-icon" aria-hidden="true"></div>
|
||||
<div class="title-area">
|
||||
<p class="popup-quarter" aria-hidden="true"><?php echo htmlspecialchars($modalQuarterLabel ?? '26년 3분기)', ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
<strong class="popup-title goal-slide-title"></strong>
|
||||
<p class="popup-desc goal-slide-desc"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recs-list goal-slide-recs">
|
||||
<div class="rec-item">
|
||||
<span class="rec-label">추천.01</span>
|
||||
<div class="rec-content">
|
||||
<strong class="rec-target"><?php echo htmlspecialchars($modalRecItems[0]['title'] ?? '', ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<p class="rec-text"><?php echo htmlspecialchars($modalRecItems[0]['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rec-item">
|
||||
<span class="rec-label">추천.02</span>
|
||||
<div class="rec-content">
|
||||
<strong class="rec-target"><?php echo htmlspecialchars($modalRecItems[1]['title'] ?? '', ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<p class="rec-text"><?php echo htmlspecialchars($modalRecItems[1]['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rec-item">
|
||||
<span class="rec-label">추천.03</span>
|
||||
<div class="rec-content">
|
||||
<strong class="rec-target"><?php echo htmlspecialchars($modalRecItems[2]['title'] ?? '', ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<p class="rec-text"><?php echo htmlspecialchars($modalRecItems[2]['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="bookshelf-section goal-slide-bookshelf" aria-label="추천 도서 책장">
|
||||
<div class="bookshelf">
|
||||
<div class="books-list books-list--same-height">
|
||||
<?php foreach (($modalBooks ?? []) as $book) : ?>
|
||||
<?php
|
||||
$subText = str_replace('\\n', "\n", (string)($book['sub'] ?? ''));
|
||||
$mainText = str_replace('\\n', "\n", (string)($book['main'] ?? ''));
|
||||
$bookId = (int)($book['id'] ?? 0);
|
||||
$bookTitle = (string)($book['title'] ?? '');
|
||||
$bookImg = (string)($book['img'] ?? 'img_book_01');
|
||||
$bookYoutube = (string)($book['youtube'] ?? '');
|
||||
?>
|
||||
<div class="books-item" data-video-id="<?php echo $bookId; ?>">
|
||||
<div class="book-img-wrap" style="--book-img: url('/img/myclass/<?php echo htmlspecialchars($bookImg, ENT_QUOTES, 'UTF-8'); ?>.png'); --book-img-m: url('/img/myclass/<?php echo htmlspecialchars($bookImg, ENT_QUOTES, 'UTF-8'); ?>_m.png')">
|
||||
<picture class="book-img-picture">
|
||||
<source media="(max-width: 767px)" srcset="/img/myclass/<?php echo htmlspecialchars($bookImg, ENT_QUOTES, 'UTF-8'); ?>_m.png">
|
||||
<img src="/img/myclass/<?php echo htmlspecialchars($bookImg, ENT_QUOTES, 'UTF-8'); ?>.png" alt="추천 도서 이미지" class="book-img">
|
||||
</picture>
|
||||
</div>
|
||||
<button type="button" class="book-info-btn" data-video-id="<?php echo $bookId; ?>" aria-label="영상 상세 보기 - <?php echo htmlspecialchars($bookTitle, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<span class="book-info-wrap">
|
||||
<div class="book-video-area">
|
||||
<span class="book-video-thumb-wrap">
|
||||
<img src="https://img.youtube.com/vi/<?php echo htmlspecialchars($bookYoutube, ENT_QUOTES, 'UTF-8'); ?>/sddefault.jpg" alt="" class="book-video-thumb">
|
||||
</span>
|
||||
<label class="bookmark" for="like_book_goal_<?php echo $bookId; ?>" onclick="event.stopPropagation();"><input type="checkbox" id="like_book_goal_<?php echo $bookId; ?>" title="저장"></label>
|
||||
</div>
|
||||
<strong class="book-title"><?php echo htmlspecialchars($bookTitle, ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<div class="book-desc">
|
||||
<p class="book-desc-sub"><?php echo nl2br(htmlspecialchars($subText, ENT_QUOTES, 'UTF-8')); ?></p>
|
||||
<p class="book-desc-main"><?php echo nl2br(htmlspecialchars($mainText, ENT_QUOTES, 'UTF-8')); ?></p>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="shelf-plank">
|
||||
<div class="shelf-tags">
|
||||
<span>1Q</span>
|
||||
<ul>
|
||||
<li>IT 테크</li>
|
||||
<li>웰니스</li>
|
||||
<li>마인드셋</li>
|
||||
<li>리더십</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // 슬라이드 1 -->
|
||||
</div>
|
||||
<!-- 하단: 목표 페이지네이션 (동그라미) -->
|
||||
<div class="popup-pagination swiper-pagination" id="popupPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- // goal-layer-main -->
|
||||
|
||||
<!-- 팝업 외부 하단 버튼 -->
|
||||
<div class="goal-layer-foot">
|
||||
<a href="/skin/myclass_list.php" class="btn-set-goal" id="btnSetGoal">이 목표로 설정하기</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // goal-layer-content -->
|
||||
|
||||
</div>
|
||||
<!-- // 목표 상세 팝업 레이어 -->
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- keyword modal -->
|
||||
<!--
|
||||
이 파일은 skin/index.php 에서 include 된다.
|
||||
bbs/index.php 가 설정한 $allKeywords, $myKeywords, $adminKeywords 변수를 사용한다.
|
||||
-->
|
||||
<div class="modal keyword" id="keywordModal" aria-hidden="true" aria-modal="true" role="dialog" aria-label="나의 키워드 설정">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button class="btn-close">
|
||||
나의 키워드 수정<i class="ico-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<ul class="keyword-tag">
|
||||
<?php
|
||||
// 현재 사용자가 선택한 키워드명 배열 (checked 상태 결정에 사용)
|
||||
$myKwNames = array_column($myKeywords ?? [], 'keyword_name');
|
||||
// 회사 추천 키워드명 배열 (disabled 처리용)
|
||||
$adminKwNames = array_column($adminKeywords ?? [], 'keyword_name');
|
||||
|
||||
foreach ($allKeywords as $idx => $kw):
|
||||
$kwName = $kw['keyword_name'];
|
||||
$isAdmin = in_array($kwName, $adminKwNames, true);
|
||||
$isChecked = !$isAdmin && in_array($kwName, $myKwNames, true);
|
||||
$itemId = 'kwtag_' . ($idx + 1);
|
||||
?>
|
||||
<li class="kw-box">
|
||||
<label
|
||||
for="<?= htmlspecialchars($itemId) ?>"
|
||||
<?= $isAdmin ? 'style="opacity:0.45;cursor:not-allowed;" title="회사 추천 키워드는 나의 키워드로 선택할 수 없습니다."' : '' ?>
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="<?= htmlspecialchars($itemId) ?>"
|
||||
<?= $isChecked ? 'checked' : '' ?>
|
||||
<?= $isAdmin ? 'disabled' : '' ?>
|
||||
/>
|
||||
#<?= htmlspecialchars($kwName) ?>
|
||||
</label>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="keyword-limit-txt">
|
||||
<span class="pc-txt"><strong>최대 3개</strong> 선택 가능</span>
|
||||
<span class="mo-txt">키워드는 <strong>최대 3개</strong>까지 선택 가능합니다.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // keyword modal -->
|
||||
@@ -0,0 +1,184 @@
|
||||
<!-- 학습 가이드 PDF 팝업 (전 페이지 공통) -->
|
||||
<?php
|
||||
$learningGuideDefaultTab = $learningGuideDefaultTab ?? 'player';
|
||||
$allowedLearningGuideKeys = [
|
||||
'player',
|
||||
'main',
|
||||
'myclass',
|
||||
'onboarding',
|
||||
'legal',
|
||||
'leadership',
|
||||
'insight',
|
||||
'biztrend',
|
||||
'mypage',
|
||||
];
|
||||
|
||||
if (!in_array($learningGuideDefaultTab, $allowedLearningGuideKeys, true)) {
|
||||
$learningGuideDefaultTab = 'player';
|
||||
}
|
||||
|
||||
$learningGuideTabAttr = static function (string $tabKey) use ($learningGuideDefaultTab): array {
|
||||
$isActive = $tabKey === $learningGuideDefaultTab;
|
||||
|
||||
return [
|
||||
'class' => $isActive ? 'learning-guide-tab is-active' : 'learning-guide-tab',
|
||||
'selected' => $isActive ? 'true' : 'false',
|
||||
];
|
||||
};
|
||||
?>
|
||||
<div
|
||||
class="learning-guide-layer hidden"
|
||||
id="learningGuideLayer"
|
||||
data-default-guide-key="<?= htmlspecialchars($learningGuideDefaultTab, ENT_QUOTES, 'UTF-8') ?>"
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="학습 가이드"
|
||||
>
|
||||
<div class="learning-guide-content">
|
||||
<div class="learning-guide-header">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close-layer"
|
||||
id="btnCloseLearningGuide"
|
||||
aria-label="닫기"
|
||||
>
|
||||
<span class="blind">닫기</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="learning-guide-body">
|
||||
<div class="learning-guide-tabs">
|
||||
<div class="swiper learning-guide-tab-swiper">
|
||||
<div class="swiper-wrapper" role="tablist" aria-label="학습 가이드 메뉴">
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('player')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="player"
|
||||
data-pdf="/guide/배움터 학습가이드_학습 플레이어.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('player')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
학습 플레이어
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('main')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="main"
|
||||
data-pdf="/guide/배움터 학습가이드_메인페이지.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('main')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
메인
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('myclass')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="myclass"
|
||||
data-pdf="/guide/배움터 학습가이드_마이클래스.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('myclass')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
마이클래스
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('onboarding')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="onboarding"
|
||||
data-pdf="/guide/배움터 학습가이드_온보딩.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('onboarding')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
온보딩
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('legal')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="legal"
|
||||
data-pdf="/guide/배움터 학습가이드_법정교육.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('legal')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
법정교육
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('leadership')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="leadership"
|
||||
data-pdf="/guide/배움터 학습가이드_리더십.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('leadership')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
리더십
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('insight')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="insight"
|
||||
data-pdf="/guide/배움터 학습가이드_인사이트.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('insight')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
인사이트
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('biztrend')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="biztrend"
|
||||
data-pdf="/guide/배움터 학습가이드_비즈트렌드.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('biztrend')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
비즈트렌드
|
||||
</button>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<button
|
||||
type="button"
|
||||
class="<?= $learningGuideTabAttr('mypage')['class'] ?>"
|
||||
role="tab"
|
||||
data-guide-key="mypage"
|
||||
data-pdf="/guide/배움터 학습가이드_마이페이지.pdf"
|
||||
aria-selected="<?= $learningGuideTabAttr('mypage')['selected'] ?>"
|
||||
aria-disabled="false"
|
||||
>
|
||||
마이페이지
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="learning-guide-viewer">
|
||||
<iframe
|
||||
id="learningGuideViewer"
|
||||
title="학습 가이드 PDF"
|
||||
src=""
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- 비디오 모달 템플릿 -->
|
||||
<div class="modal video">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="video-contents">
|
||||
<div class="video-box">
|
||||
<iframe
|
||||
id="videoFrame"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src=""
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="tit-box">
|
||||
<div class="meta">
|
||||
<span>인사이트</span>
|
||||
<em>경제와사회</em>
|
||||
</div>
|
||||
<h3>회계를 조금이라도 이해하면 인생이 달라지는 이유</h3>
|
||||
</div>
|
||||
<div class="desc">
|
||||
메타버스 시대가 열리며 우리의 삶과 정체성이 디지털 중심으로 재편되고
|
||||
있습니다.<br />
|
||||
이 변화는 새로운 기회와 동시에 사회적·윤리적 과제도 함께 가져옵니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-list">
|
||||
<div class="list-header">
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="comment-wrap">
|
||||
<div class="comment-box">
|
||||
<textarea placeholder="댓글을 작성해주세요"></textarea>
|
||||
<div class="btn-area">
|
||||
<button class="btn-cancel">취소</button>
|
||||
<button class="btn-save">작성</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// 학습완료/학습중/미진행 상태를 legal_learning_data.php에서 가져와서 표시하는 헬퍼
|
||||
require_once __DIR__ . '/../../bbs/legal_learning_data.php';
|
||||
|
||||
// content_id => 상태 매핑
|
||||
$lessonStatusMap = [];
|
||||
foreach ($chapters as $chapter) {
|
||||
foreach ($chapter['lessons'] as $lesson) {
|
||||
$lessonStatusMap[$lesson['content_id']] = [
|
||||
'status' => $lesson['status'],
|
||||
'completed' => $lesson['completed'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
require __DIR__ . '/video.php';
|
||||
@@ -0,0 +1,169 @@
|
||||
<!-- 비디오 모달 템플릿 -->
|
||||
<div class="modal video">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="video-contents">
|
||||
<div class="video-area">
|
||||
<div class="video-box">
|
||||
<iframe
|
||||
id="videoFrame"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src=""
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="tit-box">
|
||||
<div class="meta no-arrow">
|
||||
<span>법정교육</span>
|
||||
<em></em>
|
||||
</div>
|
||||
<h3>개인정보보호</h3>
|
||||
</div>
|
||||
<div class="desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-side step">
|
||||
<!-- 제목 -->
|
||||
<div class="video-header">
|
||||
<div class="tit-box">
|
||||
<h5 class="tit">현재강의</h5>
|
||||
<p class="sub-txt">
|
||||
아동학대 신고의무자 및 공공부문 종사자 아동학대 예방교육(공통편)
|
||||
</p>
|
||||
</div>
|
||||
<div class="gauge-container">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" id="gaugeFill" style="width: 50%"></div>
|
||||
<img
|
||||
class="gauge-ticks"
|
||||
src="/img/video/img_gauge_ticks.svg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="gauge-labels">
|
||||
<span class="label"> <em>3</em> /6 강</span>
|
||||
<span class="label current" id="currentValue"
|
||||
>진도율 <em>50</em>%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<!-- // 제목 -->
|
||||
<div class="video-list">
|
||||
<h5 class="tit">학습목차</h5>
|
||||
<ul class="learning-list">
|
||||
<li class="complet">
|
||||
<a href="#" class="list" data-video-id="">
|
||||
<span class="seq">1차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_learning_thumb_01.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">직무스트레스의 예방과 관리</div>
|
||||
<spna class="state">학습완료</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="complet">
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">2차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_learning_thumb_02.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">직무스트레스의 예방과 관리</div>
|
||||
<spna class="state">학습완료</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="active">
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">3차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_learning_thumb_03.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">직무스트레스의 예방과 관리</div>
|
||||
<spna class="state">학습중</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">4차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_learning_thumb_04.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">하절기 질병예방 및 안전</div>
|
||||
<spna class="state">미진행</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">5차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_learning_thumb_05.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">직무스트레스의 예방과 관리</div>
|
||||
<spna class="state">미진행</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">6차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_learning_thumb_06.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">온도습도 및 미세먼지 안정</div>
|
||||
<spna class="state">미진행</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,169 @@
|
||||
<!-- 비디오 모달 템플릿 -->
|
||||
<div class="modal video">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="video-contents">
|
||||
<div class="video-area">
|
||||
<div class="video-box">
|
||||
<iframe
|
||||
id="videoFrame"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src=""
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="tit-box">
|
||||
<div class="meta no-arrow">
|
||||
<span>온보딩</span>
|
||||
<em></em>
|
||||
</div>
|
||||
<h3>회계를 조금이라도 이해하면 인생이 달라지는 이유</h3>
|
||||
<div class="tit-right">
|
||||
<button class="btn-comment"><i class="ico-comment"></i>0</button>
|
||||
<label class="bookmark" for="chk">
|
||||
<input id="chk" type="checkbox" value="" />
|
||||
<span>북마크</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
메타버스 시대가 열리며 우리의 삶과 정체성이 디지털 중심으로 재편되고
|
||||
있습니다.<br />
|
||||
이 변화는 새로운 기회와 동시에 사회적·윤리적 과제도 함께 가져옵니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-side step">
|
||||
<!-- 제목 -->
|
||||
<div class="video-header">
|
||||
<div class="tit-box">
|
||||
<h5 class="tit">현재강의</h5>
|
||||
<p class="sub-txt">
|
||||
아동학대 신고의무자 및 공공부문 종사자 아동학대 예방교육(공통편)
|
||||
</p>
|
||||
</div>
|
||||
<div class="gauge-container">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" id="gaugeFill" style="width: 20%"></div>
|
||||
<img
|
||||
class="gauge-ticks"
|
||||
src="/img/video/img_gauge_ticks.svg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="gauge-labels">
|
||||
<span class="label"> <em>1</em> /4 강</span>
|
||||
<span class="label current" id="currentValue"
|
||||
>진도율 <em>20</em>%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<!-- // 제목 -->
|
||||
<div class="video-list">
|
||||
<h5 class="tit">학습목차</h5>
|
||||
<ul class="learning-list">
|
||||
<li class="active">
|
||||
<a href="#" class="list" data-video-id="">
|
||||
<span class="seq">1차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_essential_thumb_01.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">산업재해예방 안전수칙</div>
|
||||
<spna class="state">학습중</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="">
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">2차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_essential_thumb_02.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">축적에서 길을 찾다</div>
|
||||
<spna class="state">미진행</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="">
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">3차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_essential_thumb_03.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">천재는 잊어라</div>
|
||||
<spna class="state">학습중</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="#" class="list" data-video-id="${video.id}">
|
||||
<span class="seq">4차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="thumb">
|
||||
<img
|
||||
src="/img/video/img_essential_thumb_03.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="txt-box">
|
||||
<div class="title">유령이 된 리더들</div>
|
||||
<spna class="state">미진행</spna>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="comment-wrap">
|
||||
<!-- 🔥 드래그 리사이저 추가 -->
|
||||
<div class="comment-resizer">
|
||||
<div class="resizer-handle"></div>
|
||||
</div>
|
||||
<!-- 댓글 목록 -->
|
||||
<div class="comment-list-wrap">
|
||||
<ul class="comment-list">
|
||||
<li class="empty">등록된 댓글이 없습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- // 댓글 목록 -->
|
||||
<!-- 댓글 입력 -->
|
||||
<div class="comment-box">
|
||||
<textarea placeholder="댓글을 작성해주세요"></textarea>
|
||||
<div class="btn-area">
|
||||
<button class="btn-cancel" disabled>취소</button>
|
||||
<button class="btn-save" disabled>작성</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // 댓글 입력 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
<!-- 비디오 모달 템플릿 -->
|
||||
<div class="modal video">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="video-contents">
|
||||
<div class="video-area">
|
||||
<div class="video-box">
|
||||
<iframe
|
||||
id="videoFrame"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src=""
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="tit-box">
|
||||
<div class="meta">
|
||||
<span>인사이트</span>
|
||||
<em>경제와사회</em>
|
||||
</div>
|
||||
<h3>회계를 조금이라도 이해하면 인생이 달라지는 이유</h3>
|
||||
<div class="tit-right">
|
||||
<button class="btn-comment"><i class="ico-comment"></i>0</button>
|
||||
<label class="bookmark" for="video-bookmark-chk">
|
||||
<input id="video-bookmark-chk" type="checkbox" value="" />
|
||||
<span>북마크</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-side">
|
||||
<div class="video-header">
|
||||
<h5 class="tit">관련영상</h5>
|
||||
<span class="badge">소통</span>
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<div class="video-list">
|
||||
<ul>
|
||||
<!-- JS(loadRecommendedVideos)로 동적 렌더링 -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="comment-wrap">
|
||||
<!-- 🔥 드래그 리사이저 추가 -->
|
||||
<div class="comment-resizer">
|
||||
<div class="resizer-handle"></div>
|
||||
</div>
|
||||
<!-- 댓글 목록 -->
|
||||
<div class="comment-list-wrap">
|
||||
<ul class="comment-list">
|
||||
<!-- JS(get_comments.php)로 동적 렌더링 -->
|
||||
</ul>
|
||||
</div>
|
||||
<!-- // 댓글 목록 -->
|
||||
|
||||
<!-- 댓글 입력 -->
|
||||
<div class="comment-box">
|
||||
<textarea placeholder="댓글을 작성해주세요"></textarea>
|
||||
<div class="btn-area">
|
||||
<button class="btn-cancel" disabled>취소</button>
|
||||
<button class="btn-save" disabled>작성</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // 댓글 입력 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
// Temporary diagnostics for dash_* tables.
|
||||
// Remove this file after verification.
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$key = (string)($_GET['key'] ?? '');
|
||||
if ($key !== 'baroncs-dash-probe-20260409') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'forbidden'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../bbs/db_conn.php';
|
||||
|
||||
if (!function_exists('db_conn')) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => 'db_conn_not_found'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db_conn();
|
||||
if (!($pdo instanceof PDO)) {
|
||||
throw new RuntimeException('db_conn_not_pdo');
|
||||
}
|
||||
|
||||
$pdo->exec("SET NAMES 'utf8mb4'");
|
||||
|
||||
$tables = $pdo->query("SHOW TABLES LIKE 'dash\\_%'")->fetchAll(PDO::FETCH_NUM);
|
||||
$tableNames = array_map(static function ($r) {
|
||||
return (string)$r[0];
|
||||
}, $tables ?: []);
|
||||
|
||||
$result = [
|
||||
'ok' => true,
|
||||
'generated_at' => date('c'),
|
||||
'table_count' => count($tableNames),
|
||||
'tables' => [],
|
||||
];
|
||||
|
||||
foreach ($tableNames as $table) {
|
||||
$descStmt = $pdo->query("SHOW COLUMNS FROM `" . str_replace('`', '``', $table) . "`");
|
||||
$columns = $descStmt ? $descStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
|
||||
$countStmt = $pdo->query("SELECT COUNT(*) AS cnt FROM `" . str_replace('`', '``', $table) . "`");
|
||||
$rowCount = $countStmt ? (int)$countStmt->fetchColumn() : 0;
|
||||
|
||||
$sampleStmt = $pdo->query("SELECT * FROM `" . str_replace('`', '``', $table) . "` LIMIT 3");
|
||||
$sampleRows = $sampleStmt ? $sampleStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
|
||||
$result['tables'][] = [
|
||||
'name' => $table,
|
||||
'row_count' => $rowCount,
|
||||
'columns' => $columns,
|
||||
'sample_rows' => $sampleRows,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'exception',
|
||||
'message' => $e->getMessage(),
|
||||
'type' => get_class($e),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Temporary session diagnostics page.
|
||||
// Remove this file after verification.
|
||||
|
||||
$accessKey = 'baroncs-session-check-20260408';
|
||||
$key = (string)($_GET['key'] ?? '');
|
||||
if ($key !== $accessKey) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Forbidden";
|
||||
exit;
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$firstSeen = isset($_SESSION['diag_first_seen']) ? (int)$_SESSION['diag_first_seen'] : $now;
|
||||
$lastSeenBefore = isset($_SESSION['diag_last_seen']) ? (int)$_SESSION['diag_last_seen'] : null;
|
||||
$_SESSION['diag_first_seen'] = $firstSeen;
|
||||
$_SESSION['diag_last_seen'] = $now;
|
||||
$_SESSION['diag_hit_count'] = (int)($_SESSION['diag_hit_count'] ?? 0) + 1;
|
||||
|
||||
$cookieParams = session_get_cookie_params();
|
||||
$phpSessCookie = $_COOKIE[session_name()] ?? '';
|
||||
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Session Diagnostics</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; line-height: 1.5; }
|
||||
h1 { margin: 0 0 12px; }
|
||||
.box { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin: 12px 0; }
|
||||
.k { color: #666; display: inline-block; min-width: 240px; }
|
||||
.v { font-weight: 600; }
|
||||
.warn { color: #b54708; }
|
||||
.ok { color: #027a48; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Temporary Session Diagnostics</h1>
|
||||
<p class="warn">Check complete, then delete this file immediately.</p>
|
||||
|
||||
<div class="box">
|
||||
<div><span class="k">Server time</span><span class="v"><?php echo date('Y-m-d H:i:s T', $now); ?></span></div>
|
||||
<div><span class="k">PHP version</span><span class="v"><?php echo PHP_VERSION; ?></span></div>
|
||||
<div><span class="k">Session name</span><span class="v"><?php echo htmlspecialchars(session_name(), ENT_QUOTES, 'UTF-8'); ?></span></div>
|
||||
<div><span class="k">Session ID</span><span class="v"><?php echo htmlspecialchars(session_id(), ENT_QUOTES, 'UTF-8'); ?></span></div>
|
||||
<div><span class="k">Session save path</span><span class="v"><?php echo htmlspecialchars((string)ini_get('session.save_path'), ENT_QUOTES, 'UTF-8'); ?></span></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div><span class="k">ini: session.cookie_lifetime</span><span class="v"><?php echo (string)ini_get('session.cookie_lifetime'); ?></span></div>
|
||||
<div><span class="k">ini: session.gc_maxlifetime</span><span class="v"><?php echo (string)ini_get('session.gc_maxlifetime'); ?></span></div>
|
||||
<div><span class="k">cookie params lifetime</span><span class="v"><?php echo (string)$cookieParams['lifetime']; ?></span></div>
|
||||
<div><span class="k">cookie params path</span><span class="v"><?php echo htmlspecialchars((string)$cookieParams['path'], ENT_QUOTES, 'UTF-8'); ?></span></div>
|
||||
<div><span class="k">cookie params domain</span><span class="v"><?php echo htmlspecialchars((string)$cookieParams['domain'], ENT_QUOTES, 'UTF-8'); ?></span></div>
|
||||
<div><span class="k">cookie params secure</span><span class="v"><?php echo $cookieParams['secure'] ? 'true' : 'false'; ?></span></div>
|
||||
<div><span class="k">cookie params httponly</span><span class="v"><?php echo $cookieParams['httponly'] ? 'true' : 'false'; ?></span></div>
|
||||
<div><span class="k">cookie value present</span><span class="v"><?php echo $phpSessCookie !== '' ? 'yes' : 'no'; ?></span></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div><span class="k">First seen</span><span class="v"><?php echo date('Y-m-d H:i:s T', $firstSeen); ?></span></div>
|
||||
<div><span class="k">Last seen before this request</span><span class="v"><?php echo $lastSeenBefore ? date('Y-m-d H:i:s T', $lastSeenBefore) : 'none'; ?></span></div>
|
||||
<div><span class="k">Hit count in same session</span><span class="v"><?php echo (string)$_SESSION['diag_hit_count']; ?></span></div>
|
||||
<div><span class="k">Idle seconds since previous hit</span><span class="v"><?php echo $lastSeenBefore ? (string)max(0, $now - $lastSeenBefore) : '0'; ?></span></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<p><strong>How to test quickly</strong></p>
|
||||
<ol>
|
||||
<li>Refresh this page and verify Hit count increases.</li>
|
||||
<li>Close browser completely, reopen, access this page again, compare Session ID.</li>
|
||||
<li>Leave idle for 20-40 minutes, revisit and see if Session ID/Hit count resets.</li>
|
||||
</ol>
|
||||
<p class="ok">If Session ID changes or Hit count resets, session expired or cookie was dropped.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,271 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<?php
|
||||
$SET_PREFIX = 'I'; //L=리더십(기본값), I=인사이트
|
||||
require_once __DIR__ . '/../bbs/leadership_init_data.php';
|
||||
?>
|
||||
<body>
|
||||
<div class="wrap insight">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="insight-hero">
|
||||
<div class="insight-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.php">홈</a></li>
|
||||
<li><a href="./insight.php">인사이트</a></li>
|
||||
<li><span class="current">경제와 사회</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<?php foreach ($array_banner_img as $i => $bn): ?>
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="<?= $bn['normal'] ?>">
|
||||
<source media="(max-width: 768px)" srcset="<?= $bn['mobile'] ?>">
|
||||
<img src="<?= $bn['normal'] ?>" alt="<?= $bn['title'] ?>">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
<button type="button" class="swiper-button-prev hero-prev btn-prev" aria-label="이전 슬라이드"></button>
|
||||
<button type="button" class="swiper-button-next hero-next btn-next" aria-label="다음 슬라이드"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="카테고리 탭">
|
||||
<?php foreach ($array_tab_info as $i => $ti): ?>
|
||||
<button class="leadership-tab <?php if($i==0){?>is-active<?php }?>" type="button" role="tab" aria-selected="<?php if($i==0){?>true<?php }else{?>false<?php }?> " data-cate="<?= $ti['base_code'] ?>">
|
||||
<span class="tab-icon"><img src="<?= $ti['src'] ?>" alt="" /></span>
|
||||
<span class="tab-text"><?= $ti['code_name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="insight-video-list">
|
||||
<div class="insight-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 탭선택에 따른 동적 컨텐츠영역 -->
|
||||
<ul class="video-grid" id="video-list"></ul>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper (리더십과 동일)
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
navigation: {
|
||||
nextEl: '.hero-next',
|
||||
prevEl: '.hero-prev',
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs (리더십과 동일)
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (tabs.length) {
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
let currentText = $(this).find('.tab-text').text();
|
||||
console.log('tabs');
|
||||
console.log(currentText);
|
||||
$('.current').text(currentText);
|
||||
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
/*
|
||||
let currentText = $('.current').find('.tab-text').text();
|
||||
if(currentText==""){$('.current').text("리더십 입문");}
|
||||
*/
|
||||
let firstTabText = $('.leadership-tab.is-active').find('.tab-text').text();
|
||||
if($('.current').text() == "" && firstTabText != ""){
|
||||
$('.current').text(firstTabText);
|
||||
}
|
||||
});
|
||||
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
//let category = $('.leadership-tab.is-active').data('cate') || 'CA200L01';
|
||||
let category = $('.leadership-tab.is-active').data('cate') || 'CA200<?= $SET_PREFIX ?>01';
|
||||
let sort = $('.select-sort').val() || 'latest';
|
||||
|
||||
loadVideos();
|
||||
|
||||
$('.leadership-tab').on('click', function () {
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.select-sort').on('change', function () {
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.container').on('scroll', function () {
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = this.scrollTop;
|
||||
const windowHeight = this.clientHeight;
|
||||
const docHeight = this.scrollHeight;
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 100) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('.container').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/edu/ajax/get_video_list.php?SET_PREFIX=<?= $SET_PREFIX ?>',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
if (currentRequestSeq !== requestSeq) return;
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) page--;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) page--;
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,388 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./leadership.html">리더십</a></li>
|
||||
<li><span class="current">셀프 리더십</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_01_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_01.png" alt="실천으로 완성하는 리더십">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_02_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_02.png" alt="성장하는 리더십 여정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_03.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_03_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_03.png" alt="리더로 성장하는 과정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="리더십 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_01.svg" alt="" /></span>
|
||||
<span class="tab-text">리더십 입문111</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_02.svg" alt="" /></span>
|
||||
<span class="tab-text">셀프리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_03.svg" alt="" /></span>
|
||||
<span class="tab-text">팀리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_04.svg" alt="" /></span>
|
||||
<span class="tab-text">실전 리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_05.svg" alt="" /></span>
|
||||
<span class="tab-text">인물탐구</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list" aria-label="리더십 콘텐츠 목록">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em>8</em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option selected>조회수</option>
|
||||
<option>업데이트</option>
|
||||
<option>내가본컨텐츠</option>
|
||||
<option>안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="video-grid">
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l1" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l1" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_01.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">목표관리, 성과관리의 차이? 더 중요한 것은?</strong>
|
||||
<span class="item-desc">리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l2" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l2" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_02.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l3" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l3" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_03.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">경제경영이론을 이용한 동기부여 이론들</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l4" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">DT시대의 애자일 경영, 비즈니스 어질리티</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l1" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l1" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_01.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">목표관리, 성과관리의 차이? 더 중요한 것은?</strong>
|
||||
<span class="item-desc">리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l2" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l2" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_02.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l3" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l3" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_03.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">경제경영이론을 이용한 동기부여 이론들</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l4" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">DT시대의 애자일 경영, 비즈니스 어질리티</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l1" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l1" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_01.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">목표관리, 성과관리의 차이? 더 중요한 것은?</strong>
|
||||
<span class="item-desc">리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l2" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l2" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_02.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l3" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l3" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_03.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">경제경영이론을 이용한 동기부여 이론들</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l4" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">DT시대의 애자일 경영, 비즈니스 어질리티</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l5" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l5" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_05.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">조직문화가 저희의 가장 강력한 무기입니다...</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l6" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l6" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_06.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">꼰대가 되지 않고 건설적인 피드백을 하는 법</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l7" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l7" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_07.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">제대로 된 평가 면담, 어떻게 할 수 있을까? (SAS, 듀폰)</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l8" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l8" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">성장 마인드셋 실패에서 배우기</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,334 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./leadership.html">리더십</a></li>
|
||||
<li><span class="current">셀프 리더십</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_01_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_01.png" alt="실천으로 완성하는 리더십">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_02_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_02.png" alt="성장하는 리더십 여정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_03.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_03_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_03.png" alt="리더로 성장하는 과정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="리더십 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L01">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_01.svg" alt="" /></span>
|
||||
<span class="tab-text">리더십 입문</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true" data-cate="CA200L02">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_02.svg" alt="" /></span>
|
||||
<span class="tab-text">셀프리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L03">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_03.svg" alt="" /></span>
|
||||
<span class="tab-text">팀리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L04">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_04.svg" alt="" /></span>
|
||||
<span class="tab-text">실전 리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L05">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_05.svg" alt="" /></span>
|
||||
<span class="tab-text">인물탐구</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list" aria-label="리더십 콘텐츠 목록">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="video-grid" id="video-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
let category = $('.leadership-tab.is-active').data('cate') || 'CA200L02';
|
||||
let sort = $('.select-sort').val() || 'view';
|
||||
|
||||
loadVideos();
|
||||
|
||||
/* 탭 클릭 */
|
||||
$('.leadership-tab').on('click', function () {
|
||||
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
/* 정렬 변경 */
|
||||
$('.select-sort').on('change', function () {
|
||||
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
console.log('scrollTop='+scrollTop);
|
||||
console.log('windowHeight='+windowHeight);
|
||||
console.log('docHeight='+docHeight);
|
||||
|
||||
console.log('scrollTop + windowHeight=');
|
||||
console.log(scrollTop + windowHeight);
|
||||
|
||||
console.log('docHeight - 400=');
|
||||
console.log(docHeight - 450);
|
||||
|
||||
|
||||
|
||||
$(window).on('scroll', function () {
|
||||
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = $(window).scrollTop();
|
||||
const windowHeight = $(window).height();
|
||||
const docHeight = $(document).height();
|
||||
|
||||
|
||||
|
||||
.container
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* 무한스크롤 */
|
||||
$(window).on('scroll', function () {
|
||||
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = $(window).scrollTop();
|
||||
const windowHeight = $(window).height();
|
||||
const docHeight = $(document).height();
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 200) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('html, body').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
|
||||
if (isLoading) {
|
||||
$('#video-loading').show();
|
||||
} else {
|
||||
$('#video-loading').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/edu/ajax/get_video_list.php',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
|
||||
/* 이전 요청이 늦게 도착한 경우 무시 */
|
||||
if (currentRequestSeq !== requestSeq) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) {
|
||||
page--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
|
||||
if (page === 1 && html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) {
|
||||
page--;
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./leadership.html">리더십</a></li>
|
||||
<li><span class="current">셀프 리더십</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_01_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_01.png" alt="실천으로 완성하는 리더십">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_02_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_02.png" alt="성장하는 리더십 여정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_03.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_03_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_03.png" alt="리더로 성장하는 과정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="리더십 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L01">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_01.svg" alt="" /></span>
|
||||
<span class="tab-text">리더십 입문</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true" data-cate="CA200L02">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_02.svg" alt="" /></span>
|
||||
<span class="tab-text">셀프리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L03">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_03.svg" alt="" /></span>
|
||||
<span class="tab-text">팀리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L04">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_04.svg" alt="" /></span>
|
||||
<span class="tab-text">실전 리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L05">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_05.svg" alt="" /></span>
|
||||
<span class="tab-text">인물탐구</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list" aria-label="리더십 콘텐츠 목록">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="video-grid" id="video-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
let category = $('.leadership-tab.is-active').data('cate') || 'CA200L02';
|
||||
let sort = $('.select-sort').val() || 'latest';
|
||||
|
||||
loadVideos();
|
||||
|
||||
$('.leadership-tab').on('click', function () {
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.select-sort').on('change', function () {
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.container').on('scroll', function () {
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = this.scrollTop;
|
||||
const windowHeight = this.clientHeight;
|
||||
const docHeight = this.scrollHeight;
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 100) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('.container').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/edu/ajax/get_video_list.php',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
if (currentRequestSeq !== requestSeq) return;
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) page--;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) page--;
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,571 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="stylesheet" type="text/css" href="/edu/css/main.css" />
|
||||
|
||||
<style>
|
||||
/* 마이페이지 콘텐츠 카드 삭제 버튼 숨김
|
||||
- 정책상 기능은 유지하되, 화면에는 노출하지 않는다. */
|
||||
.mypage .btn-remove-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 마이페이지 콘텐츠 빈 상태 문구 */
|
||||
.mypage .content-empty {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* 마이페이지 한줄소감
|
||||
- 사용자명/프로필 이미지 영역 제거 후 레이아웃 최소 보정 */
|
||||
.mypage .review-comment {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text strong {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 한줄소감 빈 상태 문구 */
|
||||
.mypage .review-empty {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_01.php';
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_02.php';
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_05.php';
|
||||
?>
|
||||
<?php
|
||||
$profileData = $profileData ?? [];
|
||||
$profileBadges = $profileBadges ?? [];
|
||||
|
||||
$profileName = $profileData['name'] ?? '사용자';
|
||||
$profileRankName = $profileData['rank_name'] ?? '';
|
||||
$profileWorkingComp = $profileData['working_comp'] ?? '';
|
||||
$profileLevelLabel = $profileData['learning_level'] ?? 'Rookie';
|
||||
$profileLevelClass = $profileData['level_class'] ?? 'rookie';
|
||||
$profileLevelIcon = $profileData['level_icon'] ?? '/edu/img/ico/ico_level_rookie.svg';
|
||||
$profileTotalMinutes = (int)($profileData['total_minutes'] ?? 0);
|
||||
$profileImage = $profileData['profile_image'] ?? '/edu/img/insight/profile.png';
|
||||
|
||||
$badgeHat = $profileBadges['hat'] ?? null;
|
||||
$badgePencil = $profileBadges['pencil'] ?? null;
|
||||
$badgePick = $profileBadges['pick'] ?? null;
|
||||
?>
|
||||
<?php
|
||||
/* 제안하기 */
|
||||
$offerHistory = $offerHistory ?? [];
|
||||
$offerDefaultTypeCode = $offerDefaultTypeCode ?? 'OF10001';
|
||||
$offerDefaultStatusCode = $offerDefaultStatusCode ?? 'OF10002';
|
||||
?>
|
||||
<body>
|
||||
<!--
|
||||
========================================
|
||||
마이페이지 레이아웃 구조
|
||||
========================================
|
||||
[좌측] aside.mypage-sidebar : 프로필 + 컨텐츠 제안
|
||||
[우측] main.mypage-main : 학습 활동 + 시청/저장/소감 목록
|
||||
-->
|
||||
<div class="wrap mypage">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<div class="container">
|
||||
<div class="mypage-inner">
|
||||
<!--
|
||||
좌측 사이드바: 프로필 카드 + 컨텐츠 제안 폼
|
||||
수정 시: profile-card 내 이름, 직급, 학습레벨 등 변경
|
||||
-->
|
||||
<aside class="mypage-sidebar">
|
||||
<h2 class="mypage-title">마이페이지</h2>
|
||||
<div class="profile-area">
|
||||
<div class="profile-card">
|
||||
<div class="profile-photo-wrap">
|
||||
<div class="profile-photo">
|
||||
<!--
|
||||
<img src="/edu/img/insight/profile.png" alt="홍길동 프로필" />
|
||||
-->
|
||||
<img src="<?= htmlspecialchars($profileImage, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?> 프로필" />
|
||||
|
||||
</div>
|
||||
<!-- PC 전용 프로필 배지 아이콘 -->
|
||||
<!--
|
||||
<div class="profile-photo-badges" aria-hidden="true">
|
||||
<span class="badge-item badge-item-hat">
|
||||
<img src="/edu/img/mypage/ico_school.png" alt="" />
|
||||
</span>
|
||||
<span class="badge-item badge-item-pencil">
|
||||
<img src="/edu/img/mypage/ico_pencil.png" alt="" />
|
||||
</span>
|
||||
<span class="badge-item badge-item-pick">
|
||||
<img src="/edu/img/mypage/ico_pick_2.png" alt="" />
|
||||
</span>
|
||||
</div>
|
||||
-->
|
||||
<div class="profile-photo-badges" aria-hidden="true">
|
||||
<?php if (!empty($badgeHat)): ?>
|
||||
<span class="badge-item badge-item-hat">
|
||||
<img src="<?= htmlspecialchars($badgeHat['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgeHat['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePencil)): ?>
|
||||
<span class="badge-item badge-item-pencil">
|
||||
<img src="<?= htmlspecialchars($badgePencil['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePencil['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePick)): ?>
|
||||
<span class="badge-item badge-item-pick">
|
||||
<img src="<?= htmlspecialchars($badgePick['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePick['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<!--
|
||||
<p class="profile-name">
|
||||
<em>
|
||||
<span class="ico-level">
|
||||
<img src="/edu/img/ico/ico_level_master.svg" alt="학습레벨" />
|
||||
</span>
|
||||
홍길동
|
||||
</em>
|
||||
<span>선임연구원</span>
|
||||
</p>
|
||||
<p class="profile-role">바론컨설턴트</p>
|
||||
-->
|
||||
<p class="profile-name">
|
||||
<em>
|
||||
<span class="ico-level">
|
||||
<img src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" alt="학습레벨" />
|
||||
</span>
|
||||
<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</em>
|
||||
<span><?= htmlspecialchars($profileRankName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
|
||||
</p>
|
||||
<p class="profile-role"><?= htmlspecialchars($profileWorkingComp, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
|
||||
|
||||
<!-- badge-level master일때만 클래스명 추가 -->
|
||||
<!-- <div class="badge-level master"> -->
|
||||
<div
|
||||
class="badge-level <?= htmlspecialchars($profileLevelClass, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
id="mypage-level-box"
|
||||
>
|
||||
|
||||
<span class="badge-level-title">학습레벨</span>
|
||||
<!--
|
||||
<span class="badge-level-value">
|
||||
<i class="ico-level">
|
||||
<img src="/edu/img/ico/ico_level_master.svg" alt="학습레벨" />
|
||||
</i>
|
||||
Master
|
||||
</span>
|
||||
-->
|
||||
<span class="badge-level-value">
|
||||
<i class="ico-level">
|
||||
<img
|
||||
id="mypage-level-icon"
|
||||
src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="학습레벨"
|
||||
/>
|
||||
</i>
|
||||
<span id="mypage-level-label">
|
||||
<?= htmlspecialchars($profileLevelLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="ico-info-wrap">
|
||||
<i class="ico-info" aria-describedby="level-tooltip"></i>
|
||||
<span class="ico-info-tooltip" role="tooltip" id="level-tooltip">
|
||||
<span class="ico-info-tooltip-desc">누적된 학습 시간에 따라<br>4단계의 레벨이<br>자동 설정됩니다.</span>
|
||||
<ul class="ico-info-tooltip-list">
|
||||
<li><em>Master</em><span>40시간 이상</span></li>
|
||||
<li><em>Elite</em><span>20 ~ 40시간</span></li>
|
||||
<li><em>Learner</em><span>8 ~ 20시간</span></li>
|
||||
<li><em>Rookie</em><span>0 ~ 8시간</span></li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- MOBILE: 총 학습시간 버튼 (모바일에서만 표시, 클릭 시 활동 모달 오픈) -->
|
||||
<!--
|
||||
<button class="mobile-total-time-btn" type="button" id="mobileActivityBtn" aria-label="총 학습시간 상세보기">
|
||||
<span>총 학습시간 <strong>340</strong>분</span> <span class="mobile-total-time-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
-->
|
||||
<button class="mobile-total-time-btn" type="button" id="mobileActivityBtn" aria-label="총 학습시간 상세보기">
|
||||
<span>총 학습시간 <strong id="mobile-mypage-total-minutes"><?= number_format($profileTotalMinutes) ?></strong>분</span>
|
||||
<span class="mobile-total-time-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 컨텐츠 제안하기 Start -->
|
||||
<section class="suggest-section">
|
||||
<h3 class="suggest-title">컨텐츠 제안하기</h3>
|
||||
|
||||
<form id="offerForm" method="post" action="/edu/ajax/insert_offer.php">
|
||||
<input type="hidden" name="type_code" value="<?= htmlspecialchars($offerDefaultTypeCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
<input type="hidden" name="status_code" value="<?= htmlspecialchars($offerDefaultStatusCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
|
||||
<div class="suggest-fields">
|
||||
<input
|
||||
type="url"
|
||||
class="suggest-input suggest-url"
|
||||
placeholder="URL"
|
||||
id="suggest-url"
|
||||
name="reference_url"
|
||||
/>
|
||||
<textarea
|
||||
class="suggest-input suggest-reason"
|
||||
placeholder="추천이유"
|
||||
rows="4"
|
||||
id="suggest-reason"
|
||||
name="reason"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled>
|
||||
제안 보내기
|
||||
<i class="ico-arrow"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="suggest-status accordion">
|
||||
<button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="suggest-status-content">
|
||||
<span class="accordion-title">제안현황</span>
|
||||
<i class="ico-chevron" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div id="suggest-status-content" class="accordion-content" hidden>
|
||||
<ul class="suggest-status-list">
|
||||
<?php if (!empty($offerHistory)): ?>
|
||||
<?php foreach ($offerHistory as $row): ?>
|
||||
<li
|
||||
class="<?= !empty($row['is_consider']) ? 'consider' : '' ?>"
|
||||
data-offer-id="<?= htmlspecialchars($row['offer_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
>
|
||||
<span class="suggest-date">
|
||||
<?= htmlspecialchars($row['created_at_dot'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
|
||||
<?php if (!empty($row['is_consider'])): ?>
|
||||
<span class="suggest-dots"></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="suggest-state">
|
||||
<?= htmlspecialchars($row['status_name'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<li>
|
||||
<span class="suggest-date">-</span>
|
||||
<span class="suggest-state">등록된 제안이 없습니다.</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 컨텐츠 제안하기 End -->
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!--
|
||||
우측 메인: 학습 활동 통계 + 시청/저장/소감 목록
|
||||
학습 시간 수정: total-time-text의 strong, gauge-fill의 width 값
|
||||
-->
|
||||
<main class="mypage-main">
|
||||
<section class="activity-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin"></i>
|
||||
나의 학습 활동
|
||||
<div class="select-box">
|
||||
<label class="year-badge" style="display: none;">(2026년 기준)</label>
|
||||
<select id="mypage-year-select">
|
||||
<option>년</option>
|
||||
</select>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="total-time-area">
|
||||
<div class="total-time-inner">
|
||||
<div class="total-time">
|
||||
<p class="total-time-text">
|
||||
총 학습시간 <strong id="mypage-total-minutes"></strong>분
|
||||
</p>
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" id="mypage-total-gauge" style="width:50%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="total-average">전체평균 <em id="mypage-avg-minutes">0</em>분</span>
|
||||
</div>
|
||||
|
||||
<ul class="activity-list">
|
||||
<li class="activity-item gauge" data-category="CA10001">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">마이클래스</span>
|
||||
<span class="activity-value"><em></em>분 (0%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 33%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">6개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10002">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">온보딩</span>
|
||||
<span class="activity-value"><em>110</em>분 (80%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 80%"></div>
|
||||
<p class="activity-note" style="display:none;">
|
||||
<span class="activity-note-num">①</span> 필수 시청: <span class="onboarding-due-date">26.01.14</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">10개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10003">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">법정교육</span>
|
||||
<span class="activity-value"><em>30</em>분 (20%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">6개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10004">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">리더십</span>
|
||||
<span class="activity-value"><em>60</em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 40%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">240분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10005">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">인사이트</span>
|
||||
<span class="activity-value"><em>43</em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 60%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">90분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10006">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">비즈트렌드</span>
|
||||
<span class="activity-value"><em>23</em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">80분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="mylist-area">
|
||||
<!-- MOBILE: 2탭 네비게이션 (시청완료 <> 슬라이더 + 한줄소감) -->
|
||||
<div class="mobile-list-tab-header">
|
||||
<button class="mobile-list-tab active" type="button" id="mobileTabContent" data-mobile-tab="content" aria-selected="true">
|
||||
<span class="mobile-tab-arrow mobile-tab-prev" aria-label="이전" aria-hidden="true">‹</span>
|
||||
<span class="mobile-tab-label">시청완료</span>
|
||||
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">›</span>
|
||||
</button>
|
||||
<button class="mobile-list-tab" type="button" id="mobileTabReview" data-mobile-tab="review" aria-selected="false">
|
||||
<span class="ico-mobile-pencil" aria-hidden="true"></span>
|
||||
한줄 소감
|
||||
</button>
|
||||
</div>
|
||||
<!-- 콘텐츠 탭 영역 -->
|
||||
<div class="content-tab-wrap">
|
||||
<div class="content-tabs" role="tablist">
|
||||
<button type="button" class="content-tab active" role="tab" aria-selected="true" aria-controls="panel-watching" id="tab-watching" data-tab="watching">
|
||||
시청중인 콘텐츠 <span class="count">15</span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-completed" id="tab-completed" data-tab="completed">
|
||||
시청완료 콘텐츠 <span class="count">15</span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-saved" id="tab-saved" data-tab="saved">
|
||||
저장한 콘텐츠 <span class="count">3</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-panels">
|
||||
<!-- 시청중인 콘텐츠 -->
|
||||
<section class="content-section content-panel active" id="panel-watching" role="tabpanel" aria-labelledby="tab-watching" data-panel="watching">
|
||||
<ul class="content-grid" id="mypage-watching-list"></ul>
|
||||
</section>
|
||||
|
||||
<!-- 시청완료 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-completed" role="tabpanel" aria-labelledby="tab-completed" data-panel="completed">
|
||||
<ul class="content-grid"></ul>
|
||||
</section>
|
||||
|
||||
<!-- 저장한 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-saved" role="tabpanel" aria-labelledby="tab-saved" data-panel="saved">
|
||||
<ul class="content-grid"></ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 한줄 소감 -->
|
||||
<section class="review-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin-list" aria-hidden="true"></i>
|
||||
한줄 소감 <span class="count" id="mypage-review-count">0</span>
|
||||
</h3>
|
||||
|
||||
<ul class="review-list" id="mypage-review-list"></ul>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- MOBILE: 하단 목록 카운트 + 컨텐츠 제안 바 (모바일에서만 표시) -->
|
||||
<nav class="mobile-bottom-nav" aria-label="모바일 콘텐츠 탐색">
|
||||
<button class="mobile-nav-item mobile-nav-item--suggest" type="button" id="mobileSuggestBtn">
|
||||
<span class="mobile-nav-ico mobile-nav-ico--send" aria-hidden="true"></span>
|
||||
<span class="mobile-nav-label">컨텐츠 제안하기</span>
|
||||
<span class="mobile-nav-chevron mobile-nav-chevron--right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
|
||||
<!-- MOBILE: 학습 활동 팝업 모달 -->
|
||||
<div class="mobile-activity-modal" id="mobileActivityModal" role="dialog" aria-modal="true" aria-labelledby="mobileModalTitle" hidden>
|
||||
<div class="modal-backdrop" id="mobileActivityBackdrop"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<p class="modal-title" id="mobileModalTitle">
|
||||
<span class="titile-label">총 학습시간</span> <strong>340</strong><span>분</span></p>
|
||||
<div class="modal-gauge-wrap">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 63%"></div>
|
||||
</div>
|
||||
<span class="modal-average-badge">전체 평균</span>
|
||||
</div>
|
||||
<button class="modal-close-btn" type="button" id="mobileActivityClose" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="modal-activity-list">
|
||||
<li>
|
||||
<span class="modal-label">마이클래스</span>
|
||||
<span class="modal-value modal-value--green"><em>74</em>분(33%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">온보딩 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i> 필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>110</em>분(80%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">법정교육 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i> 필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>30</em>분(20%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">리더십</span>
|
||||
<span class="modal-value modal-value--brown"><em>60</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">인사이트</span>
|
||||
<span class="modal-value modal-value--brown"><em>43</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">비즈트렌드</span>
|
||||
<span class="modal-value modal-value--brown"><em>23</em>분</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE: 컨텐츠 제안 바텀시트 -->
|
||||
<div class="mobile-suggest-sheet" id="mobileSuggestSheet" role="dialog" aria-modal="true" hidden>
|
||||
<div class="sheet-backdrop" id="mobileSuggestBackdrop"></div>
|
||||
<div class="sheet-panel">
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-header-ico" aria-hidden="true"></span>
|
||||
<p class="sheet-title">컨텐츠 제안하기</p>
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="suggest-fields">
|
||||
<input type="url" class="suggest-input sheet-url" placeholder="URL" id="sheet-url" />
|
||||
<textarea class="suggest-input sheet-reason" placeholder="추천이유" rows="5" id="sheet-reason"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn-sheet-confirm" id="btnSheetConfirm" disabled>확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 마이페이지 전용 스크립트: 제안 폼 버튼 활성화, 제안현황 아코디언 -->
|
||||
<script src="/edu/js/mypage.js" defer></script>
|
||||
<!-- ERP기획 : 26.03.20 moon -->
|
||||
<script src="/edu/js/apply_page/add_mypage.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,542 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="stylesheet" type="text/css" href="/edu/css/main.css" />
|
||||
|
||||
<style>
|
||||
/* 마이페이지 콘텐츠 카드 삭제 버튼 숨김
|
||||
- 정책상 기능은 유지하되, 화면에는 노출하지 않는다. */
|
||||
.mypage .btn-remove-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 마이페이지 콘텐츠 빈 상태 문구 */
|
||||
.mypage .content-empty {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* 마이페이지 한줄소감
|
||||
- 사용자명/프로필 이미지 영역 제거 후 레이아웃 최소 보정 */
|
||||
.mypage .review-comment {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text strong {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 한줄소감 빈 상태 문구 */
|
||||
.mypage .review-empty {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_01.php';//개인정보
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_02.php';//제안하기 초기정보
|
||||
?>
|
||||
<?php
|
||||
$profileData = $profileData ?? [];
|
||||
$profileBadges = $profileBadges ?? [];
|
||||
|
||||
$profileName = $profileData['name'] ?? '사용자';
|
||||
$profileRankName = $profileData['rank_name'] ?? '';
|
||||
$profileWorkingComp = $profileData['working_comp'] ?? '';
|
||||
$profileBelongCompCd = $profileData['belong_comp_code'] ?? '';//소속회사 코드
|
||||
$profileBelongComp = $profileData['belong_comp_name'] ?? '';//소속회사 명
|
||||
$profileLevelLabel = $profileData['learning_level'] ?? 'Rookie';
|
||||
$profileLevelClass = $profileData['level_class'] ?? 'rookie';
|
||||
$profileLevelIcon = $profileData['level_icon'] ?? '/edu/img/ico/ico_level_rookie.svg';
|
||||
$profileTotalMinutes = (int)($profileData['total_minutes'] ?? 0);
|
||||
$profileImage = $profileData['profile_image'] ?? '/edu/img/ico/ico_user.svg';
|
||||
|
||||
$badgeHat = $profileBadges['hat'] ?? null;
|
||||
$badgePencil = $profileBadges['pencil'] ?? null;
|
||||
$badgePick = $profileBadges['pick'] ?? null;
|
||||
?>
|
||||
<?php
|
||||
/* 제안하기 */
|
||||
$offerHistory = $offerHistory ?? [];
|
||||
$offerDefaultTypeCode = $offerDefaultTypeCode ?? 'OF10001';
|
||||
$offerDefaultStatusCode = $offerDefaultStatusCode ?? 'OF10002';
|
||||
?>
|
||||
<body>
|
||||
<!--
|
||||
========================================
|
||||
마이페이지 레이아웃 구조
|
||||
========================================
|
||||
[좌측] aside.mypage-sidebar : 프로필 + 컨텐츠 제안
|
||||
[우측] main.mypage-main : 학습 활동 + 시청/저장/소감 목록
|
||||
-->
|
||||
<div class="wrap mypage">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<div class="container">
|
||||
<div class="mypage-inner">
|
||||
<!--
|
||||
좌측 사이드바: 프로필 카드 + 컨텐츠 제안 폼
|
||||
수정 시: profile-card 내 이름, 직급, 학습레벨 등 변경
|
||||
-->
|
||||
<aside class="mypage-sidebar">
|
||||
<h2 class="mypage-title">마이페이지</h2>
|
||||
<div class="profile-area">
|
||||
<div class="profile-card">
|
||||
<div class="profile-photo-wrap">
|
||||
<!--
|
||||
<div class="profile-photo">
|
||||
<img src="<?= htmlspecialchars($profileImage, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?> 프로필" referrerpolicy="no-referrer"/>
|
||||
</div>
|
||||
-->
|
||||
<!--
|
||||
<div class="profile-photo" id="profile-upload-trigger" style="cursor:pointer;">
|
||||
-->
|
||||
<div class="profile-photo" id="" style="cursor:pointer;">
|
||||
<img id="profile-image" src="<?= $profileImage ?>">
|
||||
</div>
|
||||
<input type="file" id="profile-file-input" accept="image/png, image/jpeg" style="display:none;">
|
||||
|
||||
<!-- PC 전용 프로필 배지 아이콘 -->
|
||||
<div class="profile-photo-badges" aria-hidden="true">
|
||||
<?php if (!empty($badgeHat)): ?>
|
||||
<span class="badge-item badge-item-hat">
|
||||
<img src="<?= htmlspecialchars($badgeHat['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgeHat['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePencil)): ?>
|
||||
<span class="badge-item badge-item-pencil">
|
||||
<img src="<?= htmlspecialchars($badgePencil['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePencil['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePick)): ?>
|
||||
<span class="badge-item badge-item-pick">
|
||||
<img src="<?= htmlspecialchars($badgePick['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePick['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="profile-name">
|
||||
<em>
|
||||
<span class="ico-level">
|
||||
<img src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" alt="학습레벨" />
|
||||
</span>
|
||||
<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</em>
|
||||
<span><?= htmlspecialchars($profileRankName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
|
||||
</p>
|
||||
<p class="profile-role"><?= htmlspecialchars($profileBelongComp, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
|
||||
|
||||
<!-- badge-level master일때만 클래스명 추가 -->
|
||||
<!-- <div class="badge-level master"> -->
|
||||
<div
|
||||
class="badge-level <?= htmlspecialchars($profileLevelClass, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
id="mypage-level-box"
|
||||
>
|
||||
|
||||
<span class="badge-level-title">학습레벨</span>
|
||||
|
||||
<span class="badge-level-value">
|
||||
<i class="ico-level">
|
||||
<img
|
||||
id="mypage-level-icon"
|
||||
src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="학습레벨"
|
||||
/>
|
||||
</i>
|
||||
<span id="mypage-level-label">
|
||||
<?= htmlspecialchars($profileLevelLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="ico-info-wrap">
|
||||
<i class="ico-info" aria-describedby="level-tooltip"></i>
|
||||
<span class="ico-info-tooltip" role="tooltip" id="level-tooltip">
|
||||
<span class="ico-info-tooltip-desc">누적된 학습 시간에 따라<br>4단계의 레벨이<br>자동 설정됩니다.</span>
|
||||
<ul class="ico-info-tooltip-list">
|
||||
<li><em>Master</em><span>40시간 이상</span></li>
|
||||
<li><em>Elite</em><span>20 ~ 40시간</span></li>
|
||||
<li><em>Learner</em><span>8 ~ 20시간</span></li>
|
||||
<li><em>Rookie</em><span>0 ~ 8시간</span></li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- MOBILE: 총 학습시간 버튼 (모바일에서만 표시, 클릭 시 활동 모달 오픈) -->
|
||||
<button class="mobile-total-time-btn" type="button" id="mobileActivityBtn" aria-label="총 학습시간 상세보기">
|
||||
<span>총 학습시간 <strong id="mobile-mypage-total-minutes"><?= number_format($profileTotalMinutes) ?></strong>분</span>
|
||||
<span class="mobile-total-time-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 컨텐츠 제안하기 Start -->
|
||||
<section class="suggest-section">
|
||||
<h3 class="suggest-title">컨텐츠 제안하기</h3>
|
||||
|
||||
<form id="offerForm" method="post" action="/edu/ajax/insert_offer.php">
|
||||
<input type="hidden" name="type_code" value="<?= htmlspecialchars($offerDefaultTypeCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
<input type="hidden" name="status_code" value="<?= htmlspecialchars($offerDefaultStatusCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
|
||||
<div class="suggest-fields">
|
||||
<input
|
||||
type="url"
|
||||
class="suggest-input suggest-url"
|
||||
placeholder="URL"
|
||||
id="suggest-url"
|
||||
name="reference_url"
|
||||
/>
|
||||
<textarea
|
||||
class="suggest-input suggest-reason"
|
||||
placeholder="추천이유"
|
||||
rows="4"
|
||||
id="suggest-reason"
|
||||
name="reason"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled>
|
||||
제안 보내기
|
||||
<i class="ico-arrow"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="suggest-status accordion">
|
||||
<button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="suggest-status-content">
|
||||
<span class="accordion-title">제안현황</span>
|
||||
<i class="ico-chevron" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div id="suggest-status-content" class="accordion-content" hidden>
|
||||
<ul class="suggest-status-list">
|
||||
<?php if (!empty($offerHistory)): ?>
|
||||
<?php foreach ($offerHistory as $row): ?>
|
||||
<li
|
||||
class="<?= !empty($row['is_consider']) ? 'consider' : '' ?>"
|
||||
data-offer-id="<?= htmlspecialchars($row['offer_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
>
|
||||
<span class="suggest-date">
|
||||
<?= htmlspecialchars($row['created_at_dot'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
|
||||
<?php if (!empty($row['is_consider'])): ?>
|
||||
<span class="suggest-dots"></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="suggest-state">
|
||||
<?= htmlspecialchars($row['status_name'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<li>
|
||||
<span class="suggest-date">-</span>
|
||||
<span class="suggest-state">등록된 제안이 없습니다.</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 컨텐츠 제안하기 End -->
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!--
|
||||
우측 메인: 학습 활동 통계 + 시청/저장/소감 목록
|
||||
학습 시간 수정: total-time-text의 strong, gauge-fill의 width 값
|
||||
-->
|
||||
<main class="mypage-main">
|
||||
<section class="activity-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin"></i>
|
||||
나의 학습 활동
|
||||
<div class="select-box">
|
||||
<label class="year-badge" style="display: none;"></label>
|
||||
<select id="mypage-year-select">
|
||||
<option>년</option>
|
||||
</select>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="total-time-area">
|
||||
<div class="total-time-inner">
|
||||
<div class="total-time">
|
||||
<p class="total-time-text">
|
||||
총 학습시간 <strong id="mypage-total-minutes"></strong>분
|
||||
</p>
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" id="mypage-total-gauge" style="width:50%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="total-average">전체평균 <em id="mypage-avg-minutes">0</em>분</span>
|
||||
</div>
|
||||
|
||||
<ul class="activity-list">
|
||||
<li class="activity-item gauge" data-category="CA10001">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">마이클래스</span>
|
||||
<span class="activity-value"><em></em>분 (0%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 33%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10002">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">온보딩</span>
|
||||
<span class="activity-value"><em></em>분 (80%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 80%"></div>
|
||||
<p class="activity-note" style="display:none;">
|
||||
<span class="activity-note-num">①</span> 필수 시청: <span class="onboarding-due-date">26.01.14</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10003">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">법정교육</span>
|
||||
<span class="activity-value"><em></em>분 (20%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10004">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">리더십</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 40%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10005">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">인사이트</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 60%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10006">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">비즈트렌드</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="mylist-area">
|
||||
<!-- MOBILE: 2탭 네비게이션 (시청완료 <> 슬라이더 + 한줄소감) -->
|
||||
<div class="mobile-list-tab-header">
|
||||
<button class="mobile-list-tab active" type="button" id="mobileTabContent" data-mobile-tab="content" aria-selected="true">
|
||||
<span class="mobile-tab-arrow mobile-tab-prev" aria-label="이전" aria-hidden="true">‹</span>
|
||||
<span class="mobile-tab-label">시청완료</span>
|
||||
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">›</span>
|
||||
</button>
|
||||
<button class="mobile-list-tab" type="button" id="mobileTabReview" data-mobile-tab="review" aria-selected="false">
|
||||
<span class="ico-mobile-pencil" aria-hidden="true"></span>
|
||||
한줄 소감
|
||||
</button>
|
||||
</div>
|
||||
<!-- 콘텐츠 탭 영역 -->
|
||||
<div class="content-tab-wrap">
|
||||
<div class="content-tabs" role="tablist">
|
||||
<button type="button" class="content-tab active" role="tab" aria-selected="true" aria-controls="panel-watching" id="tab-watching" data-tab="watching">
|
||||
시청중인 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-completed" id="tab-completed" data-tab="completed">
|
||||
시청완료 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-saved" id="tab-saved" data-tab="saved">
|
||||
저장한 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-panels">
|
||||
<!-- 시청중인 콘텐츠 -->
|
||||
<section class="content-section content-panel active" id="panel-watching" role="tabpanel" aria-labelledby="tab-watching" data-panel="watching">
|
||||
<ul class="content-grid" id="mypage-watching-list"></ul>
|
||||
</section>
|
||||
|
||||
<!-- 시청완료 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-completed" role="tabpanel" aria-labelledby="tab-completed" data-panel="completed">
|
||||
<ul class="content-grid" id="mypage-completed-list"></ul>
|
||||
</section>
|
||||
<!-- 저장한 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-saved" role="tabpanel" aria-labelledby="tab-saved" data-panel="saved">
|
||||
<ul class="content-grid" id="mypage-saved-list"></ul>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 한줄 소감 -->
|
||||
<section class="review-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin-list" aria-hidden="true"></i>
|
||||
한줄 소감 <span class="count" id="mypage-review-count">0</span>
|
||||
</h3>
|
||||
|
||||
<ul class="review-list" id="mypage-review-list"></ul>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- MOBILE: 하단 목록 카운트 + 컨텐츠 제안 바 (모바일에서만 표시) -->
|
||||
<nav class="mobile-bottom-nav" aria-label="모바일 콘텐츠 탐색">
|
||||
<button class="mobile-nav-item mobile-nav-item--suggest" type="button" id="mobileSuggestBtn">
|
||||
<span class="mobile-nav-ico mobile-nav-ico--send" aria-hidden="true"></span>
|
||||
<span class="mobile-nav-label">컨텐츠 제안하기</span>
|
||||
<span class="mobile-nav-chevron mobile-nav-chevron--right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
|
||||
<!-- MOBILE: 학습 활동 팝업 모달 -->
|
||||
<div class="mobile-activity-modal" id="mobileActivityModal" role="dialog" aria-modal="true" aria-labelledby="mobileModalTitle" hidden>
|
||||
<div class="modal-backdrop" id="mobileActivityBackdrop"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<p class="modal-title" id="mobileModalTitle">
|
||||
<span class="titile-label">총 학습시간</span> <strong>340</strong><span>분</span></p>
|
||||
<div class="modal-gauge-wrap">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 63%"></div>
|
||||
</div>
|
||||
<span class="modal-average-badge">전체 평균</span>
|
||||
</div>
|
||||
<button class="modal-close-btn" type="button" id="mobileActivityClose" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="modal-activity-list">
|
||||
<li>
|
||||
<span class="modal-label">마이클래스</span>
|
||||
<span class="modal-value modal-value--green"><em>74</em>분(33%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">온보딩 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i> 필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>110</em>분(80%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">법정교육 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i> 필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>30</em>분(20%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">리더십</span>
|
||||
<span class="modal-value modal-value--brown"><em>60</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">인사이트</span>
|
||||
<span class="modal-value modal-value--brown"><em>43</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">비즈트렌드</span>
|
||||
<span class="modal-value modal-value--brown"><em>23</em>분</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE: 컨텐츠 제안 바텀시트 -->
|
||||
<div class="mobile-suggest-sheet" id="mobileSuggestSheet" role="dialog" aria-modal="true" hidden>
|
||||
<div class="sheet-backdrop" id="mobileSuggestBackdrop"></div>
|
||||
<div class="sheet-panel">
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-header-ico" aria-hidden="true"></span>
|
||||
<p class="sheet-title">컨텐츠 제안하기</p>
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="suggest-fields">
|
||||
<input type="url" class="suggest-input sheet-url" placeholder="URL" id="sheet-url" />
|
||||
<textarea class="suggest-input sheet-reason" placeholder="추천이유" rows="5" id="sheet-reason"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn-sheet-confirm" id="btnSheetConfirm" disabled>확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 마이페이지 전용 스크립트: 제안 폼 버튼 활성화, 제안현황 아코디언 -->
|
||||
<script src="/edu/js/mypage.js" defer></script>
|
||||
<!-- ERP기획 : 26.03.20 moon -->
|
||||
<script src="/edu/js/apply_page/add_mypage.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,536 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="stylesheet" type="text/css" href="/edu/css/main.css" />
|
||||
|
||||
<style>
|
||||
/* 마이페이지 콘텐츠 카드 삭제 버튼 숨김
|
||||
- 정책상 기능은 유지하되, 화면에는 노출하지 않는다. */
|
||||
.mypage .btn-remove-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 마이페이지 콘텐츠 빈 상태 문구 */
|
||||
.mypage .content-empty {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* 마이페이지 한줄소감
|
||||
- 사용자명/프로필 이미지 영역 제거 후 레이아웃 최소 보정 */
|
||||
.mypage .review-comment {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text strong {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 한줄소감 빈 상태 문구 */
|
||||
.mypage .review-empty {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_01.php';//개인정보
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_02.php';//제안하기 초기정보
|
||||
?>
|
||||
<?php
|
||||
$profileData = $profileData ?? [];
|
||||
$profileBadges = $profileBadges ?? [];
|
||||
|
||||
$profileName = $profileData['name'] ?? '사용자';
|
||||
$profileRankName = $profileData['rank_name'] ?? '';
|
||||
$profileWorkingComp = $profileData['working_comp'] ?? '';
|
||||
$profileBelongCompCd = $profileData['belong_comp_code'] ?? '';//소속회사 코드
|
||||
$profileBelongComp = $profileData['belong_comp_name'] ?? '';//소속회사 명
|
||||
$profileLevelLabel = $profileData['learning_level'] ?? 'Rookie';
|
||||
$profileLevelClass = $profileData['level_class'] ?? 'rookie';
|
||||
$profileLevelIcon = $profileData['level_icon'] ?? '/edu/img/ico/ico_level_rookie.svg';
|
||||
$profileTotalMinutes = (int)($profileData['total_minutes'] ?? 0);
|
||||
$profileImage = $profileData['profile_image'] ?? '/edu/img/ico/ico_user.svg';
|
||||
|
||||
$badgeHat = $profileBadges['hat'] ?? null;
|
||||
$badgePencil = $profileBadges['pencil'] ?? null;
|
||||
$badgePick = $profileBadges['pick'] ?? null;
|
||||
?>
|
||||
<?php
|
||||
/* 제안하기 */
|
||||
$offerHistory = $offerHistory ?? [];
|
||||
$offerDefaultTypeCode = $offerDefaultTypeCode ?? 'OF10001';
|
||||
$offerDefaultStatusCode = $offerDefaultStatusCode ?? 'OF10002';
|
||||
?>
|
||||
<body>
|
||||
<!--
|
||||
========================================
|
||||
마이페이지 레이아웃 구조
|
||||
========================================
|
||||
[좌측] aside.mypage-sidebar : 프로필 + 컨텐츠 제안
|
||||
[우측] main.mypage-main : 학습 활동 + 시청/저장/소감 목록
|
||||
-->
|
||||
<div class="wrap mypage">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<div class="container">
|
||||
<div class="mypage-inner">
|
||||
<!--
|
||||
좌측 사이드바: 프로필 카드 + 컨텐츠 제안 폼
|
||||
수정 시: profile-card 내 이름, 직급, 학습레벨 등 변경
|
||||
-->
|
||||
<aside class="mypage-sidebar">
|
||||
<h2 class="mypage-title">마이페이지</h2>
|
||||
<div class="profile-area">
|
||||
<div class="profile-card">
|
||||
<div class="profile-photo-wrap">
|
||||
<div class="profile-photo">
|
||||
<!--
|
||||
<img src="/edu/img/insight/profile.png" alt="홍길동 프로필" />
|
||||
-->
|
||||
<img src="<?= htmlspecialchars($profileImage, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?> 프로필" referrerpolicy="no-referrer"/>
|
||||
|
||||
</div>
|
||||
<!-- PC 전용 프로필 배지 아이콘 -->
|
||||
<div class="profile-photo-badges" aria-hidden="true">
|
||||
<?php if (!empty($badgeHat)): ?>
|
||||
<span class="badge-item badge-item-hat">
|
||||
<img src="<?= htmlspecialchars($badgeHat['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgeHat['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePencil)): ?>
|
||||
<span class="badge-item badge-item-pencil">
|
||||
<img src="<?= htmlspecialchars($badgePencil['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePencil['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePick)): ?>
|
||||
<span class="badge-item badge-item-pick">
|
||||
<img src="<?= htmlspecialchars($badgePick['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePick['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="profile-name">
|
||||
<em>
|
||||
<span class="ico-level">
|
||||
<img src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" alt="학습레벨" />
|
||||
</span>
|
||||
<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</em>
|
||||
<span><?= htmlspecialchars($profileRankName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
|
||||
</p>
|
||||
<p class="profile-role"><?= htmlspecialchars($profileBelongComp, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
|
||||
|
||||
<!-- badge-level master일때만 클래스명 추가 -->
|
||||
<!-- <div class="badge-level master"> -->
|
||||
<div
|
||||
class="badge-level <?= htmlspecialchars($profileLevelClass, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
id="mypage-level-box"
|
||||
>
|
||||
|
||||
<span class="badge-level-title">학습레벨</span>
|
||||
|
||||
<span class="badge-level-value">
|
||||
<i class="ico-level">
|
||||
<img
|
||||
id="mypage-level-icon"
|
||||
src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="학습레벨"
|
||||
/>
|
||||
</i>
|
||||
<span id="mypage-level-label">
|
||||
<?= htmlspecialchars($profileLevelLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="ico-info-wrap">
|
||||
<i class="ico-info" aria-describedby="level-tooltip"></i>
|
||||
<span class="ico-info-tooltip" role="tooltip" id="level-tooltip">
|
||||
<span class="ico-info-tooltip-desc">누적된 학습 시간에 따라<br>4단계의 레벨이<br>자동 설정됩니다.</span>
|
||||
<ul class="ico-info-tooltip-list">
|
||||
<li><em>Master</em><span>40시간 이상</span></li>
|
||||
<li><em>Elite</em><span>20 ~ 40시간</span></li>
|
||||
<li><em>Learner</em><span>8 ~ 20시간</span></li>
|
||||
<li><em>Rookie</em><span>0 ~ 8시간</span></li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- MOBILE: 총 학습시간 버튼 (모바일에서만 표시, 클릭 시 활동 모달 오픈) -->
|
||||
<button class="mobile-total-time-btn" type="button" id="mobileActivityBtn" aria-label="총 학습시간 상세보기">
|
||||
<span>총 학습시간 <strong id="mobile-mypage-total-minutes"><?= number_format($profileTotalMinutes) ?></strong>분</span>
|
||||
<span class="mobile-total-time-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 컨텐츠 제안하기 Start -->
|
||||
<section class="suggest-section">
|
||||
<h3 class="suggest-title">컨텐츠 제안하기</h3>
|
||||
|
||||
<form id="offerForm" method="post" action="/edu/ajax/insert_offer.php">
|
||||
<input type="hidden" name="type_code" value="<?= htmlspecialchars($offerDefaultTypeCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
<input type="hidden" name="status_code" value="<?= htmlspecialchars($offerDefaultStatusCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
|
||||
<div class="suggest-fields">
|
||||
<input
|
||||
type="url"
|
||||
class="suggest-input suggest-url"
|
||||
placeholder="URL"
|
||||
id="suggest-url"
|
||||
name="reference_url"
|
||||
/>
|
||||
<textarea
|
||||
class="suggest-input suggest-reason"
|
||||
placeholder="추천이유"
|
||||
rows="4"
|
||||
id="suggest-reason"
|
||||
name="reason"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled>
|
||||
제안 보내기
|
||||
<i class="ico-arrow"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="suggest-status accordion">
|
||||
<button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="suggest-status-content">
|
||||
<span class="accordion-title">제안현황</span>
|
||||
<i class="ico-chevron" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div id="suggest-status-content" class="accordion-content" hidden>
|
||||
<ul class="suggest-status-list">
|
||||
<?php if (!empty($offerHistory)): ?>
|
||||
<?php foreach ($offerHistory as $row): ?>
|
||||
<li
|
||||
class="<?= !empty($row['is_consider']) ? 'consider' : '' ?>"
|
||||
data-offer-id="<?= htmlspecialchars($row['offer_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
>
|
||||
<span class="suggest-date">
|
||||
<?= htmlspecialchars($row['created_at_dot'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
|
||||
<?php if (!empty($row['is_consider'])): ?>
|
||||
<span class="suggest-dots"></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="suggest-state">
|
||||
<?= htmlspecialchars($row['status_name'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<li>
|
||||
<span class="suggest-date">-</span>
|
||||
<span class="suggest-state">등록된 제안이 없습니다.</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 컨텐츠 제안하기 End -->
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!--
|
||||
우측 메인: 학습 활동 통계 + 시청/저장/소감 목록
|
||||
학습 시간 수정: total-time-text의 strong, gauge-fill의 width 값
|
||||
-->
|
||||
<main class="mypage-main">
|
||||
<section class="activity-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin"></i>
|
||||
나의 학습 활동
|
||||
<div class="select-box">
|
||||
<label class="year-badge" style="display: none;"></label>
|
||||
<select id="mypage-year-select">
|
||||
<option>년</option>
|
||||
</select>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="total-time-area">
|
||||
<div class="total-time-inner">
|
||||
<div class="total-time">
|
||||
<p class="total-time-text">
|
||||
총 학습시간 <strong id="mypage-total-minutes"></strong>분
|
||||
</p>
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" id="mypage-total-gauge" style="width:50%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="total-average">전체평균 <em id="mypage-avg-minutes">0</em>분</span>
|
||||
</div>
|
||||
|
||||
<ul class="activity-list">
|
||||
<li class="activity-item gauge" data-category="CA10001">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">마이클래스</span>
|
||||
<span class="activity-value"><em></em>분 (0%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 33%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10002">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">온보딩</span>
|
||||
<span class="activity-value"><em></em>분 (80%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 80%"></div>
|
||||
<p class="activity-note" style="display:none;">
|
||||
<span class="activity-note-num">①</span> 필수 시청: <span class="onboarding-due-date">26.01.14</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10003">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">법정교육</span>
|
||||
<span class="activity-value"><em></em>분 (20%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10004">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">리더십</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 40%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10005">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">인사이트</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 60%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10006">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">비즈트렌드</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="mylist-area">
|
||||
<!-- MOBILE: 2탭 네비게이션 (시청완료 <> 슬라이더 + 한줄소감) -->
|
||||
<div class="mobile-list-tab-header">
|
||||
<button class="mobile-list-tab active" type="button" id="mobileTabContent" data-mobile-tab="content" aria-selected="true">
|
||||
<span class="mobile-tab-arrow mobile-tab-prev" aria-label="이전" aria-hidden="true">‹</span>
|
||||
<span class="mobile-tab-label">시청완료</span>
|
||||
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">›</span>
|
||||
</button>
|
||||
<button class="mobile-list-tab" type="button" id="mobileTabReview" data-mobile-tab="review" aria-selected="false">
|
||||
<span class="ico-mobile-pencil" aria-hidden="true"></span>
|
||||
한줄 소감
|
||||
</button>
|
||||
</div>
|
||||
<!-- 콘텐츠 탭 영역 -->
|
||||
<div class="content-tab-wrap">
|
||||
<div class="content-tabs" role="tablist">
|
||||
<button type="button" class="content-tab active" role="tab" aria-selected="true" aria-controls="panel-watching" id="tab-watching" data-tab="watching">
|
||||
시청중인 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-completed" id="tab-completed" data-tab="completed">
|
||||
시청완료 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-saved" id="tab-saved" data-tab="saved">
|
||||
저장한 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-panels">
|
||||
<!-- 시청중인 콘텐츠 -->
|
||||
<section class="content-section content-panel active" id="panel-watching" role="tabpanel" aria-labelledby="tab-watching" data-panel="watching">
|
||||
<ul class="content-grid" id="mypage-watching-list"></ul>
|
||||
</section>
|
||||
|
||||
<!-- 시청완료 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-completed" role="tabpanel" aria-labelledby="tab-completed" data-panel="completed">
|
||||
<ul class="content-grid" id="mypage-completed-list"></ul>
|
||||
</section>
|
||||
<!-- 저장한 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-saved" role="tabpanel" aria-labelledby="tab-saved" data-panel="saved">
|
||||
<ul class="content-grid" id="mypage-saved-list"></ul>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 한줄 소감 -->
|
||||
<section class="review-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin-list" aria-hidden="true"></i>
|
||||
한줄 소감 <span class="count" id="mypage-review-count">0</span>
|
||||
</h3>
|
||||
|
||||
<ul class="review-list" id="mypage-review-list"></ul>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- MOBILE: 하단 목록 카운트 + 컨텐츠 제안 바 (모바일에서만 표시) -->
|
||||
<nav class="mobile-bottom-nav" aria-label="모바일 콘텐츠 탐색">
|
||||
<button class="mobile-nav-item mobile-nav-item--suggest" type="button" id="mobileSuggestBtn">
|
||||
<span class="mobile-nav-ico mobile-nav-ico--send" aria-hidden="true"></span>
|
||||
<span class="mobile-nav-label">컨텐츠 제안하기</span>
|
||||
<span class="mobile-nav-chevron mobile-nav-chevron--right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
|
||||
<!-- MOBILE: 학습 활동 팝업 모달 -->
|
||||
<div class="mobile-activity-modal" id="mobileActivityModal" role="dialog" aria-modal="true" aria-labelledby="mobileModalTitle" hidden>
|
||||
<div class="modal-backdrop" id="mobileActivityBackdrop"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<p class="modal-title" id="mobileModalTitle">
|
||||
<span class="titile-label">총 학습시간</span> <strong>340</strong><span>분</span></p>
|
||||
<div class="modal-gauge-wrap">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 63%"></div>
|
||||
</div>
|
||||
<span class="modal-average-badge">전체 평균</span>
|
||||
</div>
|
||||
<button class="modal-close-btn" type="button" id="mobileActivityClose" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="modal-activity-list">
|
||||
<li>
|
||||
<span class="modal-label">마이클래스</span>
|
||||
<span class="modal-value modal-value--green"><em>74</em>분(33%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">온보딩 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i> 필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>110</em>분(80%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">법정교육 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i> 필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>30</em>분(20%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">리더십</span>
|
||||
<span class="modal-value modal-value--brown"><em>60</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">인사이트</span>
|
||||
<span class="modal-value modal-value--brown"><em>43</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">비즈트렌드</span>
|
||||
<span class="modal-value modal-value--brown"><em>23</em>분</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE: 컨텐츠 제안 바텀시트 -->
|
||||
<div class="mobile-suggest-sheet" id="mobileSuggestSheet" role="dialog" aria-modal="true" hidden>
|
||||
<div class="sheet-backdrop" id="mobileSuggestBackdrop"></div>
|
||||
<div class="sheet-panel">
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-header-ico" aria-hidden="true"></span>
|
||||
<p class="sheet-title">컨텐츠 제안하기</p>
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="suggest-fields">
|
||||
<input type="url" class="suggest-input sheet-url" placeholder="URL" id="sheet-url" />
|
||||
<textarea class="suggest-input sheet-reason" placeholder="추천이유" rows="5" id="sheet-reason"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn-sheet-confirm" id="btnSheetConfirm" disabled>확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 마이페이지 전용 스크립트: 제안 폼 버튼 활성화, 제안현황 아코디언 -->
|
||||
<script src="/edu/js/mypage.js" defer></script>
|
||||
<!-- ERP기획 : 26.03.20 moon -->
|
||||
<script src="/edu/js/apply_page/add_mypage.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+1287
File diff suppressed because it is too large
Load Diff
+119
@@ -0,0 +1,119 @@
|
||||
<div class="guide-wrap">
|
||||
<svg class="guide-svg" width="100%" height="100%">
|
||||
<defs>
|
||||
<mask id="guide-mask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
||||
<path id="guide-cutout-path" fill="black" />
|
||||
<path id="guide-arc-stroke-mask-path" fill="black" />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="rgba(0, 0, 0, 0.5)"
|
||||
mask="url(#guide-mask)"
|
||||
/>
|
||||
<path
|
||||
id="guide-stroke-path"
|
||||
fill="none"
|
||||
stroke="#FFF"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-dasharray="4"
|
||||
/>
|
||||
<path
|
||||
id="guide-arc-ellipse-stroke-path"
|
||||
fill="none"
|
||||
stroke="#FFF"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="4"
|
||||
/>
|
||||
<!-- 요소 간 연결 라인 -->
|
||||
<g id="guide-element-connection-lines" stroke="#FFF" stroke-width="1" stroke-dasharray="0" fill="none" />
|
||||
</svg>
|
||||
|
||||
<div class="guide-mask nav"></div>
|
||||
</div>
|
||||
|
||||
<!-- JS 파일 import -->
|
||||
<script src="/js/main/MultiGuide.js"></script>
|
||||
<script>
|
||||
// 전역 변수로 선언하여 다른 스크립트에서도 사용 가능하도록 함
|
||||
window.guideTargets = window.guideTargets || [
|
||||
{
|
||||
selector: ".guide-mask.nav",
|
||||
label: "메뉴",
|
||||
description: "마이클래스 : 분기별 업데이트 되는 권장 학습\n온보딩/법정교육 : 정해진 기간 내 이수해야 하는 필수 교육\n리더십/인사이트/비즈트렌드 : 지속적으로 업데이트 되는 자율 학습",
|
||||
position: "left bottom",
|
||||
class:"menu"
|
||||
},
|
||||
{
|
||||
selector: ".input-group",
|
||||
label: "검색",
|
||||
description: "2개 단어 이상 검색 시 띄어쓰기로 구분해 검색",
|
||||
position: "left bottom",
|
||||
class:"search"
|
||||
},
|
||||
{
|
||||
selector: ".alerts",
|
||||
label: "공지사항",
|
||||
description: "중요한 \n공지·알림 확인",
|
||||
position: "right bottom",
|
||||
class:"notice"
|
||||
},
|
||||
{
|
||||
selector: ".mypage",
|
||||
label: "마이페이지",
|
||||
description: "나의 학습현황·최근 시청·저장한 콘텐츠·한줄소감을 한눈에 확인",
|
||||
position: "left bottom",
|
||||
class:"mypage"
|
||||
},
|
||||
{
|
||||
selector: "#gauge",
|
||||
useGaugeArc: true,
|
||||
gaugeSize: 832,
|
||||
gaugeStrokeWidth: 31,
|
||||
gaugePadding: 20,
|
||||
padding: 0,
|
||||
startAngle: 180,
|
||||
endAngle: 360,
|
||||
label: "내 학습 vs 회사 평균",
|
||||
description: "내 학습 정도를 회사 평균과 한눈에 비교",
|
||||
position: "top center",
|
||||
class:"learning"
|
||||
},
|
||||
{
|
||||
selector: ".keyword-area",
|
||||
label: "나의 관심 키워드 + 회사추천 키워드",
|
||||
description: "키워드 버튼으로 ON/OFF, 나의 관심 키워드 편집가능",
|
||||
position: "left bottom",
|
||||
class:"interest"
|
||||
},
|
||||
{
|
||||
selector: "#nextBtn",
|
||||
label: "컨텐츠 변경",
|
||||
description: "버튼을 클릭하여 다음 컨텐츠 확인",
|
||||
position: "right bottom",
|
||||
class:"change"
|
||||
},
|
||||
// 🔥 키워드 요소 - 마스크만 (label 없음)
|
||||
{
|
||||
selector: ".video-card:nth-child(3) .key-badge:first-child",
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
selector: ".video-card:nth-child(5) .key-badge:first-child",
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
];
|
||||
// 가이드 초기화
|
||||
if (window.initMultiGuide) {
|
||||
window.initMultiGuide(window.guideTargets);
|
||||
}
|
||||
</script>
|
||||
+705
@@ -0,0 +1,705 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,532 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,439 @@
|
||||
<!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">
|
||||
<span><em>홍길동</em> 선임연구원님</span>
|
||||
<div class="keyword-area">
|
||||
<button id="keywordSettingsBtn">
|
||||
나의 키워드 <i class="ico-setting"></i>
|
||||
</button>
|
||||
<div class="keyword-list" id="myKeyword">
|
||||
<label class="kw-allow" for="chk1">
|
||||
<input type="checkbox" id="chk1" />
|
||||
#인물
|
||||
</label>
|
||||
<label class="kw-allow" for="chk2">
|
||||
<input type="checkbox" id="chk2" checked />
|
||||
#소통
|
||||
</label>
|
||||
<label class="kw-allow" for="chk3">
|
||||
<input type="checkbox" id="chk3" checked />
|
||||
#협업
|
||||
</label>
|
||||
</div>
|
||||
<div class="keyword-list">
|
||||
<label class="kw-deny" for="chk4">
|
||||
<input type="checkbox" id="chk4" checked />
|
||||
#마인드셋
|
||||
</label>
|
||||
<label class="kw-deny" for="chk5">
|
||||
<input type="checkbox" id="chk5" />
|
||||
#웰니스
|
||||
</label>
|
||||
</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 -->
|
||||
<?php include(__DIR__ . "/_modal/keyword.php") ?>
|
||||
<!-- // keyword modal -->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
<?php include(__DIR__ . "/guide.php") ?>
|
||||
</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() {
|
||||
const videos = [
|
||||
{
|
||||
id: 1,
|
||||
url: "KE_MeQZgnPM",
|
||||
category: "리더십",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: true,
|
||||
title: "정선근 교수가 알려주는 목디스크 지식",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "comment",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: "IeF8r0ycgVg",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "꼰대가 되지 않고 건설적인 피드백을 하는 법",
|
||||
picker: "",
|
||||
type: "onboarding",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: "KMZXMI0QPoA",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업",
|
||||
picker: "",
|
||||
type: "learning",
|
||||
keywords: ["마인드셋", "중간관리자"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: "CRKwszz6l2M",
|
||||
category: "인사이트",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "프로와 아마추어를 가르는 차이점!",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: "Gf5WoZ3BmgI",
|
||||
category: "비즈트렌드",
|
||||
subcate: "성공예감",
|
||||
bookmark: false,
|
||||
title: "01/16 - 트럼프 반도체 관세…한국 기업 불똥?",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["팔로우십", "AI"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
url: "KE_MeQZgnPM",
|
||||
category: "리더십",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "정선근 교수가 알려주는 목디스크 지식",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
url: "IeF8r0ycgVg",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "꼰대가 되지 않고 건설적인 피드백을 하는 법",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
];
|
||||
|
||||
// 렌더러 초기화
|
||||
const renderer = new VideoCardRenderer({
|
||||
animationDelay: 50,
|
||||
});
|
||||
|
||||
// 슬라이더 초기화
|
||||
const slider = new VideoSlider({
|
||||
videos: videos,
|
||||
videosPerPage: 6,
|
||||
onPageChange: (pageVideos) => {
|
||||
renderer.renderCards(pageVideos);
|
||||
},
|
||||
});
|
||||
|
||||
// 비디오 모달 매니저 초기화
|
||||
const modalManager = new VideoModalManager({
|
||||
videos: videos,
|
||||
});
|
||||
|
||||
// 슬라이더 초기화 (첫 페이지 렌더링 포함)
|
||||
slider.init();
|
||||
renderer.renderCards(slider.getCurrentPageVideos());
|
||||
|
||||
// 모달 매니저 초기화 (카드 클릭 이벤트 등록)
|
||||
modalManager.init();
|
||||
|
||||
// ========================================
|
||||
// 게이지 차트 초기화
|
||||
// ========================================
|
||||
|
||||
const gauge = new GaugeChart({
|
||||
size: 832,
|
||||
strokeWidth: 31,
|
||||
maxValue: 50,
|
||||
padding: 20,
|
||||
outerTextOffset: 6,
|
||||
innerTextOffset: 35,
|
||||
dotRadius: 7,
|
||||
});
|
||||
|
||||
// 게이지 초기화
|
||||
gauge.init();
|
||||
|
||||
// 예시: 학습 시간 업데이트 (35분)
|
||||
gauge.update(25);
|
||||
|
||||
// ========================================
|
||||
// MultiGuide 초기화 (게이지 초기화 후)
|
||||
// ========================================
|
||||
// 게이지와 모든 요소가 준비된 후 MultiGuide 초기화
|
||||
setTimeout(() => {
|
||||
console.log("[index_guide] MultiGuide 초기화 시도...");
|
||||
console.log("[index_guide] window.initMultiGuide 존재:", typeof window.initMultiGuide);
|
||||
console.log("[index_guide] .guide-wrap 존재:", document.querySelector(".guide-wrap") !== null);
|
||||
|
||||
if (window.initMultiGuide) {
|
||||
// guide.html에서 정의된 guideTargets 사용
|
||||
const targets = window.guideTargets || [];
|
||||
if (targets.length > 0) {
|
||||
window.initMultiGuide(targets);
|
||||
} else {
|
||||
console.warn("[index_guide] guideTargets가 정의되지 않았습니다.");
|
||||
}
|
||||
} else {
|
||||
console.warn("[index_guide] ❌ MultiGuide 초기화 함수를 찾을 수 없습니다.");
|
||||
console.warn("[index_guide] guide.html이 제대로 include되었는지 확인하세요.");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// ========================================
|
||||
// 키워드 모달 기능
|
||||
// ========================================
|
||||
|
||||
// DOM 요소
|
||||
const modal = document.getElementById("keywordModal");
|
||||
const settingsBtn = document.getElementById("keywordSettingsBtn");
|
||||
const checkIcon = modal.querySelector(".modal-header .btn-close");
|
||||
|
||||
// 현재 선택된 키워드 저장
|
||||
let allowKeywords = ["인물", "소통", "협업"]; // 초기값
|
||||
const maxAllowKeywords = 3;
|
||||
|
||||
// 모달 체크박스 상태 동기화
|
||||
function syncModalCheckboxes() {
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
checkbox.checked = allowKeywords.includes(keywordText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 체크박스 활성화/비활성화 상태 업데이트
|
||||
function updateCheckboxStates() {
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
const isMaxReached = allowKeywords.length >= maxAllowKeywords;
|
||||
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
const isSelected = allowKeywords.includes(keywordText);
|
||||
|
||||
if (!isSelected && isMaxReached) {
|
||||
checkbox.disabled = true;
|
||||
label.style.opacity = "0.5";
|
||||
label.style.cursor = "not-allowed";
|
||||
} else {
|
||||
checkbox.disabled = false;
|
||||
label.style.opacity = "1";
|
||||
label.style.cursor = "pointer";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 메인 화면 키워드 표시 업데이트
|
||||
function updateMainKeywordDisplay() {
|
||||
const allowArea = document.querySelector("#myKeyword");
|
||||
if (allowArea) {
|
||||
allowArea.innerHTML = "";
|
||||
|
||||
allowKeywords.forEach((keyword, index) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "kw-allow";
|
||||
label.setAttribute("for", `chk_allow_${index}`);
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = `chk_allow_${index}`;
|
||||
checkbox.checked = true;
|
||||
|
||||
const text = document.createTextNode(`#${keyword}`);
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(text);
|
||||
allowArea.appendChild(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 최대 개수 초과 메시지
|
||||
function showLimitMessage() {
|
||||
const modalHeader = modal.querySelector(".modal-header");
|
||||
if (modalHeader) {
|
||||
const existingMsg = modalHeader.querySelector(".limit-message");
|
||||
if (existingMsg) return;
|
||||
|
||||
const message = document.createElement("span");
|
||||
message.className = "limit-message";
|
||||
message.textContent = ` (최대 ${maxAllowKeywords}개까지 선택 가능)`;
|
||||
message.style.color = "#ff4444";
|
||||
message.style.fontSize = "14px";
|
||||
message.style.marginLeft = "10px";
|
||||
modalHeader.appendChild(message);
|
||||
|
||||
setTimeout(() => {
|
||||
message.remove();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 팝업 열기
|
||||
settingsBtn.onclick = () => {
|
||||
syncModalCheckboxes();
|
||||
updateCheckboxStates();
|
||||
modal.style.opacity = "";
|
||||
modal.style.display = "block";
|
||||
};
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = "none";
|
||||
updateMainKeywordDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
// 체크 아이콘 클릭 시 닫기
|
||||
if (checkIcon) {
|
||||
checkIcon.onclick = () => {
|
||||
modal.style.display = "none";
|
||||
updateMainKeywordDisplay();
|
||||
};
|
||||
}
|
||||
|
||||
// 모달 내 체크박스 이벤트
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
checkbox.onchange = () => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
|
||||
// 최대 개수 체크
|
||||
if (checkbox.checked && allowKeywords.length >= maxAllowKeywords) {
|
||||
checkbox.checked = false;
|
||||
showLimitMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkbox.disabled) {
|
||||
// 키워드 추가/제거
|
||||
if (checkbox.checked) {
|
||||
if (!allowKeywords.includes(keywordText)) {
|
||||
allowKeywords.push(keywordText);
|
||||
}
|
||||
} else {
|
||||
allowKeywords = allowKeywords.filter((k) => k !== keywordText);
|
||||
}
|
||||
updateCheckboxStates();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}); // DOMContentLoaded 종료
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,413 @@
|
||||
<!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">
|
||||
<span><em>홍길동</em> 선임연구원님</span>
|
||||
<div class="keyword-area">
|
||||
<button id="keywordSettingsBtn">
|
||||
나의 키워드 <i class="ico-setting"></i>
|
||||
</button>
|
||||
<div class="keyword-list" id="myKeyword">
|
||||
<label class="kw-allow" for="chk1">
|
||||
<input type="checkbox" id="chk1" />
|
||||
#인물
|
||||
</label>
|
||||
<label class="kw-allow" for="chk2">
|
||||
<input type="checkbox" id="chk2" checked />
|
||||
#소통
|
||||
</label>
|
||||
<label class="kw-allow" for="chk3">
|
||||
<input type="checkbox" id="chk3" checked />
|
||||
#협업
|
||||
</label>
|
||||
</div>
|
||||
<div class="keyword-list">
|
||||
<label class="kw-deny" for="chk4">
|
||||
<input type="checkbox" id="chk4" checked />
|
||||
#마인드셋
|
||||
</label>
|
||||
<label class="kw-deny" for="chk5">
|
||||
<input type="checkbox" id="chk5" />
|
||||
#웰니스
|
||||
</label>
|
||||
</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 -->
|
||||
<?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() {
|
||||
const videos = [
|
||||
{
|
||||
id: 1,
|
||||
url: "KE_MeQZgnPM",
|
||||
category: "리더십",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: true,
|
||||
title: "정선근 교수가 알려주는 목디스크 지식",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "comment",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: "IeF8r0ycgVg",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "꼰대가 되지 않고 건설적인 피드백을 하는 법",
|
||||
picker: "",
|
||||
type: "onboarding",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: "KMZXMI0QPoA",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업",
|
||||
picker: "",
|
||||
type: "learning",
|
||||
keywords: ["마인드셋", "중간관리자"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: "CRKwszz6l2M",
|
||||
category: "인사이트",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "프로와 아마추어를 가르는 차이점!",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: "Gf5WoZ3BmgI",
|
||||
category: "비즈트렌드",
|
||||
subcate: "성공예감",
|
||||
bookmark: false,
|
||||
title: "01/16 - 트럼프 반도체 관세…한국 기업 불똥?",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["팔로우십", "AI"],
|
||||
gauge: 0,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
url: "KE_MeQZgnPM",
|
||||
category: "리더십",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "정선근 교수가 알려주는 목디스크 지식",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "행복과 건강",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
url: "IeF8r0ycgVg",
|
||||
category: "리더십",
|
||||
subcate: "피플스토리",
|
||||
bookmark: false,
|
||||
title: "꼰대가 되지 않고 건설적인 피드백을 하는 법",
|
||||
picker: "",
|
||||
type: "main",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
];
|
||||
|
||||
// 렌더러 초기화
|
||||
const renderer = new VideoCardRenderer({
|
||||
animationDelay: 50,
|
||||
});
|
||||
|
||||
// 슬라이더 초기화
|
||||
const slider = new VideoSlider({
|
||||
videos: videos,
|
||||
videosPerPage: 6,
|
||||
onPageChange: (pageVideos) => {
|
||||
renderer.renderCards(pageVideos);
|
||||
},
|
||||
});
|
||||
|
||||
// 비디오 모달 매니저 초기화
|
||||
const modalManager = new VideoModalManager({
|
||||
videos: videos,
|
||||
});
|
||||
|
||||
// 슬라이더 초기화 (첫 페이지 렌더링 포함)
|
||||
slider.init();
|
||||
renderer.renderCards(slider.getCurrentPageVideos());
|
||||
|
||||
// 모달 매니저 초기화 (카드 클릭 이벤트 등록)
|
||||
modalManager.init();
|
||||
|
||||
// ========================================
|
||||
// 게이지 차트 초기화
|
||||
// ========================================
|
||||
|
||||
const gauge = new GaugeChart({
|
||||
size: 832,
|
||||
strokeWidth: 31,
|
||||
maxValue: 50,
|
||||
padding: 20,
|
||||
outerTextOffset: 6,
|
||||
innerTextOffset: 35,
|
||||
dotRadius: 7,
|
||||
});
|
||||
|
||||
// 게이지 초기화
|
||||
gauge.init();
|
||||
|
||||
// 예시: 학습 시간 업데이트 (35분)
|
||||
gauge.update(65);
|
||||
|
||||
// ========================================
|
||||
// 키워드 모달 기능
|
||||
// ========================================
|
||||
|
||||
// DOM 요소
|
||||
const modal = document.getElementById("keywordModal");
|
||||
const settingsBtn = document.getElementById("keywordSettingsBtn");
|
||||
const checkIcon = modal.querySelector(".modal-header .btn-close");
|
||||
|
||||
// 현재 선택된 키워드 저장
|
||||
let allowKeywords = ["인물", "소통", "협업"]; // 초기값
|
||||
const maxAllowKeywords = 3;
|
||||
|
||||
// 모달 체크박스 상태 동기화
|
||||
function syncModalCheckboxes() {
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
checkbox.checked = allowKeywords.includes(keywordText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 체크박스 활성화/비활성화 상태 업데이트
|
||||
function updateCheckboxStates() {
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
const isMaxReached = allowKeywords.length >= maxAllowKeywords;
|
||||
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
const isSelected = allowKeywords.includes(keywordText);
|
||||
|
||||
if (!isSelected && isMaxReached) {
|
||||
checkbox.disabled = true;
|
||||
label.style.opacity = "0.5";
|
||||
label.style.cursor = "not-allowed";
|
||||
} else {
|
||||
checkbox.disabled = false;
|
||||
label.style.opacity = "1";
|
||||
label.style.cursor = "pointer";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 메인 화면 키워드 표시 업데이트
|
||||
function updateMainKeywordDisplay() {
|
||||
const allowArea = document.querySelector("#myKeyword");
|
||||
if (allowArea) {
|
||||
allowArea.innerHTML = "";
|
||||
|
||||
allowKeywords.forEach((keyword, index) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "kw-allow";
|
||||
label.setAttribute("for", `chk_allow_${index}`);
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = `chk_allow_${index}`;
|
||||
checkbox.checked = true;
|
||||
|
||||
const text = document.createTextNode(`#${keyword}`);
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(text);
|
||||
allowArea.appendChild(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 최대 개수 초과 메시지
|
||||
function showLimitMessage() {
|
||||
const modalHeader = modal.querySelector(".modal-header");
|
||||
if (modalHeader) {
|
||||
const existingMsg = modalHeader.querySelector(".limit-message");
|
||||
if (existingMsg) return;
|
||||
|
||||
const message = document.createElement("span");
|
||||
message.className = "limit-message";
|
||||
message.textContent = ` (최대 ${maxAllowKeywords}개까지 선택 가능)`;
|
||||
message.style.color = "#ff4444";
|
||||
message.style.fontSize = "14px";
|
||||
message.style.marginLeft = "10px";
|
||||
modalHeader.appendChild(message);
|
||||
|
||||
setTimeout(() => {
|
||||
message.remove();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 팝업 열기
|
||||
settingsBtn.onclick = () => {
|
||||
syncModalCheckboxes();
|
||||
updateCheckboxStates();
|
||||
modal.style.opacity = "";
|
||||
modal.style.display = "block";
|
||||
};
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = "none";
|
||||
updateMainKeywordDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
// 체크 아이콘 클릭 시 닫기
|
||||
if (checkIcon) {
|
||||
checkIcon.onclick = () => {
|
||||
modal.style.display = "none";
|
||||
updateMainKeywordDisplay();
|
||||
};
|
||||
}
|
||||
|
||||
// 모달 내 체크박스 이벤트
|
||||
const modalCheckboxes = modal.querySelectorAll(
|
||||
".keyword-tag input[type='checkbox']"
|
||||
);
|
||||
modalCheckboxes.forEach((checkbox) => {
|
||||
checkbox.onchange = () => {
|
||||
const label = checkbox.closest("label");
|
||||
if (label) {
|
||||
const keywordText = label.textContent
|
||||
.trim()
|
||||
.replace("#", "")
|
||||
.trim();
|
||||
|
||||
// 최대 개수 체크
|
||||
if (checkbox.checked && allowKeywords.length >= maxAllowKeywords) {
|
||||
checkbox.checked = false;
|
||||
showLimitMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkbox.disabled) {
|
||||
// 키워드 추가/제거
|
||||
if (checkbox.checked) {
|
||||
if (!allowKeywords.includes(keywordText)) {
|
||||
allowKeywords.push(keywordText);
|
||||
}
|
||||
} else {
|
||||
allowKeywords = allowKeywords.filter((k) => k !== keywordText);
|
||||
}
|
||||
updateCheckboxStates();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}); // DOMContentLoaded 종료
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1537
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,271 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<?php
|
||||
$SET_PREFIX = 'I'; //L=리더십(기본값), I=인사이트
|
||||
require_once __DIR__ . '/../bbs/leadership_init_data.php';
|
||||
?>
|
||||
<body>
|
||||
<div class="wrap insight">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="insight-hero">
|
||||
<div class="insight-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.php">홈</a></li>
|
||||
<li><a href="./insight.php">인사이트</a></li>
|
||||
<li><span class="current">경제와 사회</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<?php foreach ($array_banner_img as $i => $bn): ?>
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="<?= $bn['normal'] ?>">
|
||||
<source media="(max-width: 768px)" srcset="<?= $bn['mobile'] ?>">
|
||||
<img src="<?= $bn['normal'] ?>" alt="<?= $bn['title'] ?>">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
<button type="button" class="swiper-button-prev hero-prev btn-prev" aria-label="이전 슬라이드"></button>
|
||||
<button type="button" class="swiper-button-next hero-next btn-next" aria-label="다음 슬라이드"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="카테고리 탭">
|
||||
<?php foreach ($array_tab_info as $i => $ti): ?>
|
||||
<button class="leadership-tab <?php if($i==0){?>is-active<?php }?>" type="button" role="tab" aria-selected="<?php if($i==0){?>true<?php }else{?>false<?php }?> " data-cate="<?= $ti['base_code'] ?>">
|
||||
<span class="tab-icon"><img src="<?= $ti['src'] ?>" alt="" /></span>
|
||||
<span class="tab-text"><?= $ti['code_name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="insight-video-list">
|
||||
<div class="insight-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 탭선택에 따른 동적 컨텐츠영역 -->
|
||||
<ul class="video-grid" id="video-list"></ul>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper (리더십과 동일)
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
navigation: {
|
||||
nextEl: '.hero-next',
|
||||
prevEl: '.hero-prev',
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs (리더십과 동일)
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (tabs.length) {
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
let currentText = $(this).find('.tab-text').text();
|
||||
console.log('tabs');
|
||||
console.log(currentText);
|
||||
$('.current').text(currentText);
|
||||
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
/*
|
||||
let currentText = $('.current').find('.tab-text').text();
|
||||
if(currentText==""){$('.current').text("리더십 입문");}
|
||||
*/
|
||||
let firstTabText = $('.leadership-tab.is-active').find('.tab-text').text();
|
||||
if($('.current').text() == "" && firstTabText != ""){
|
||||
$('.current').text(firstTabText);
|
||||
}
|
||||
});
|
||||
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
//let category = $('.leadership-tab.is-active').data('cate') || 'CA200L01';
|
||||
let category = $('.leadership-tab.is-active').data('cate') || 'CA200<?= $SET_PREFIX ?>01';
|
||||
let sort = $('.select-sort').val() || 'latest';
|
||||
|
||||
loadVideos();
|
||||
|
||||
$('.leadership-tab').on('click', function () {
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.select-sort').on('change', function () {
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.container').on('scroll', function () {
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = this.scrollTop;
|
||||
const windowHeight = this.clientHeight;
|
||||
const docHeight = this.scrollHeight;
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 100) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('.container').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/edu/ajax/get_video_list.php?SET_PREFIX=<?= $SET_PREFIX ?>',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
if (currentRequestSeq !== requestSeq) return;
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) page--;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) page--;
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap insight">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="insight-hero">
|
||||
<div class="insight-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./insight.html">인사이트</a></li>
|
||||
<li><span class="current">경제와 사회</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/img/insight/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/img/insight/img_banner_01_m.png">
|
||||
<img src="/img/insight/img_banner_01.png" alt="배너 1">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/img/insight/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/img/insight/img_banner_02_m.png">
|
||||
<img src="/img/insight/img_banner_02.png" alt="배너 2">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
<button type="button" class="swiper-button-prev hero-prev btn-prev" aria-label="이전 슬라이드"></button>
|
||||
<button type="button" class="swiper-button-next hero-next btn-next" aria-label="다음 슬라이드"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="인사이트 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/img/ico/ico_economy.svg" alt="" /></span>
|
||||
<span class="tab-text">경제와 사회</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true">
|
||||
<span class="tab-icon"><img src="/img/ico/ico_future.svg" alt="" /></span>
|
||||
<span class="tab-text">기술과미래</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/img/ico/ico_trend.svg" alt="" /></span>
|
||||
<span class="tab-text">트렌드업</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/img/ico/ico_health.svg" alt="" /></span>
|
||||
<span class="tab-text">행복과 건강</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/img/ico/ico_people.svg" alt="" /></span>
|
||||
<span class="tab-text">피플스토리</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/img/ico/ico_life.svg" alt="" /></span>
|
||||
<span class="tab-text">라이프</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="insight-video-list">
|
||||
<div class="insight-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em>8</em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option>조회수</option>
|
||||
<option>업데이트</option>
|
||||
<option>내가본컨텐츠</option>
|
||||
<option>안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="video-grid">
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i1" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i1" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_01.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">TPU가 GPU 제압할까? 데이터센터 전력전쟁이 온다</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i2" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i2" checked title="좋아요 취소"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_02.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">한국에서 가장 유명한 회계사의 조언</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i3" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i3" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_03.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">일자리 멸종하는 AI 시대를 대비하라</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i4" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i4" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_04.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">상대의 숨은 욕구를 찾아내는 것, 거기서 시작해야 됩니다</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i5" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i5" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_05.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">20년만에 구글검색의 최대강적이 나타났다</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i6" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i6" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_06.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">사람의 속마음을 한눈에 꿰뚫어보는 18가지 심리법칙</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i7" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i7" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_thumb_07.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">직장인이 쉬운 일만 하면 안되는 이유</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_i8" onclick="event.stopPropagation();"><input type="checkbox" id="like_chk_i8" title="좋아요"></label>
|
||||
<div class="item-thumb"><img src="/img/video/img_learning_thumb_01.png" alt="" /></div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">대학 부적응자가 일본 건너가 만든 일본 국민앱 '타임트리'</strong>
|
||||
<div class="tag-list">
|
||||
<span class="tag">웰니스</span>
|
||||
<span class="tag">협업</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper (리더십과 동일)
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
navigation: {
|
||||
nextEl: '.hero-next',
|
||||
prevEl: '.hero-prev',
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs (리더십과 동일)
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (tabs.length) {
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="stylesheet" type="text/css" href="/css/intro.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap intro">
|
||||
<!-- =============================================
|
||||
인트로 페이지 구조
|
||||
- Section 1: 사용자 인사 (타이핑 애니메이션)
|
||||
- Section 2: 업데이트 소개 + 기능 카드 4종
|
||||
- Section 3: CTA (시작 버튼)
|
||||
============================================= -->
|
||||
<div class="container">
|
||||
<!-- 메인 애니메이션 섹션 (Section 1, 2 공통) -->
|
||||
<section class="text-ani">
|
||||
<!-- 텍스트 영역: 인사 ↔ 업데이트 소개 전환 -->
|
||||
<div class="text-area">
|
||||
<!-- [1] Section 1: 사용자 맞춤 인사 (타이핑 효과) -->
|
||||
<div id="section1Text" class="greeting" data-section="1">
|
||||
<p>
|
||||
<span class="name" id="typedName"></span
|
||||
><span class="cursor" id="cursor">|</span>
|
||||
<br class="m-br" />안녕하세요!
|
||||
</p>
|
||||
<p class="welcome" id="welcome"></p>
|
||||
</div>
|
||||
|
||||
<!-- [2] Section 2: 업데이트 소개 텍스트 (애니메이션 순차 노출) -->
|
||||
<div id="section2Text" class="update-text hidden" data-section="2">
|
||||
<div class="update-line">
|
||||
<span id="line1">이번 업데이트에서</span>
|
||||
</div>
|
||||
<div class="update-line">
|
||||
<span id="line2"
|
||||
>더 <em>편리하고 즐겁게 학습</em><br class="m-br"> 하실 수 있도록</span
|
||||
>
|
||||
</div>
|
||||
<div class="update-line bold">
|
||||
<span id="line3">배움터가 이렇게 달라졌어요</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 콘텐츠 영역: 기능 카드 4종 (Section 2에서만 노출) -->
|
||||
<div class="content-area">
|
||||
<div class="card-list" id="cardsContainer">
|
||||
<div class="card" id="card1" data-card="1">
|
||||
<div class="card-num">01</div>
|
||||
<div class="card-title">
|
||||
<em>내 학습</em>을 <em>차곡차곡</em><br />
|
||||
<em>모아</em>보세요
|
||||
</div>
|
||||
<div class="card-desc">
|
||||
<em>"콘텐츠를 저장"</em>해두고<br />
|
||||
언제든 다시<br />
|
||||
참고할 수 있어요.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" id="card2" data-card="2">
|
||||
<div class="card-num">02</div>
|
||||
<div class="card-title">
|
||||
<em>중요한 공지사항</em>도<br />
|
||||
놓치지 않아요
|
||||
</div>
|
||||
<div class="card-desc">
|
||||
상단 <em>"알림기능"</em>을 통해<br />
|
||||
새 소식을 빠르게<br />
|
||||
확인할 수 있어요
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" id="card3" data-card="3">
|
||||
<div class="card-num">03</div>
|
||||
<div class="card-title">
|
||||
좋은 학습 <em>콘텐츠를</em><br />
|
||||
<em>공유</em>할 수 있어요
|
||||
</div>
|
||||
<div class="card-desc">
|
||||
팀원들과 함께<br />
|
||||
학습할 수 있도록<br />
|
||||
<em>"콘텐츠를 제안"</em>해보세요
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" id="card4" data-card="4">
|
||||
<div class="card-num">04</div>
|
||||
<div class="card-title">
|
||||
나의 학습 현황을<br />
|
||||
<em>확인</em>할 수 있어요
|
||||
</div>
|
||||
<div class="card-desc">
|
||||
마이페이지에서<br />
|
||||
학습현황을<br />
|
||||
<em>"한 눈에 확인"</em>해보세요
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- [3] Section 3: CTA 영역 (시작 버튼 + 마스크 효과) -->
|
||||
<div id="section3Text" class="cta-text hidden" data-section="3">
|
||||
<!-- 메인 배경 텍스트 영역 -->
|
||||
<div id="mainContainer">
|
||||
<div class="text-section" id="ctaBg">
|
||||
<p id="ctaLine1">지금부터 <em>나만의 학습으로</em></p>
|
||||
<p id="ctaLine2">성장해 볼까요?</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 마스크 오버레이 (클릭 시 확대되며 하단 텍스트 노출) -->
|
||||
<div class="mask" id="maskContainer">
|
||||
<div class="text-section">
|
||||
<p class="mask-text" id="maskLine1">
|
||||
계속 <em>성장해 나가는 배움터</em>를
|
||||
</p>
|
||||
<p class="mask-text" id="maskLine2">지켜봐 주세요!</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- CTA 버튼 -->
|
||||
<button
|
||||
class="cta-btn"
|
||||
id="ctaBtn"
|
||||
onclick="location.href = '/skin/index.php';"
|
||||
>
|
||||
약속
|
||||
<i class="ico-arrow"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 스크롤 안내 (Section 1, 2에서만 표시) -->
|
||||
<div class="scroll-indicator" id="scrollIndicator">
|
||||
<span>Scroll</span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =============================================
|
||||
인트로 설정 및 상태 (animation.js 등에서 사용)
|
||||
※ fullName, welcomeText 등 여기서 수정
|
||||
============================================= -->
|
||||
<script>
|
||||
/** 인트로 페이지 설정 값 */
|
||||
const INTRO_CONFIG = {
|
||||
// PHP 세션을 사용하여 "이름 + 직책 + 님" 형태로 조합
|
||||
fullName: "<?= $_SESSION['member_name'] . ' ' . $_SESSION['rank_name'] ?>님",
|
||||
|
||||
welcomeText:
|
||||
"<em>새로워진</em> <em>배움터</em>에 오신 것을 <em>환영합니다</em>",
|
||||
typingSpeed: 100,
|
||||
welcomeCharSpeed: 60,
|
||||
};
|
||||
|
||||
/** 인트로 페이지 상태 (섹션, 애니메이션 등) */
|
||||
const INTRO_STATE = {
|
||||
currentSection: 0, // 0:인사, 1:업데이트, 2:CTA
|
||||
typedIndex: 0,
|
||||
isScrolling: false,
|
||||
isAnimating: false,
|
||||
section2AnimDone: false,
|
||||
autoScrollTimer: null,
|
||||
lastInteractionTime: Date.now(),
|
||||
};
|
||||
|
||||
/** 사용자 이름 변경 시 호출 */
|
||||
function setUserName(name) {
|
||||
INTRO_CONFIG.fullName = name;
|
||||
}
|
||||
|
||||
/** 설정 객체 병합 (일괄 수정 시) */
|
||||
function updateConfig(config) {
|
||||
Object.assign(INTRO_CONFIG, config);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 인트로 스크립트 (로드 순서: animation → section → events → init) -->
|
||||
<script src="/js/intro/animation.js" defer></script>
|
||||
<script src="/js/intro/section.js" defer></script>
|
||||
<script src="/js/intro/events.js" defer></script>
|
||||
<script src="/js/intro/init.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+1499
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,964 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<?php
|
||||
$SET_PREFIX = 'L'; //L=리더십(기본값), I=인사이트
|
||||
$activeCate = isset($_GET['cate']) ? trim($_GET['cate']) : '';
|
||||
require_once __DIR__ . '/../bbs/category_init_data.php';
|
||||
?>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.php">홈</a></li>
|
||||
<li><a href="./leadership.php">리더십</a></li>
|
||||
<li><span class="current"></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
|
||||
<?php foreach ($array_banner_img as $i => $bn): ?>
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="<?= $bn['normal'] ?>">
|
||||
<source media="(max-width: 768px)" srcset="<?= $bn['mobile'] ?>">
|
||||
<img src="<?= $bn['normal'] ?>" alt="<?= $bn['title'] ?>">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="카테고리 탭">
|
||||
|
||||
<?php foreach ($array_tab_info as $i => $ti):
|
||||
$isActive = ($activeCate !== '') ? ($ti['base_code'] === $activeCate) : ($i === 0);
|
||||
?>
|
||||
<button class="leadership-tab <?php if($isActive){?>is-active<?php }?>" type="button" role="tab" aria-selected="<?php if($isActive){?>true<?php }else{?>false<?php }?> " data-cate="<?= $ti['base_code'] ?>">
|
||||
<span class="tab-icon"><img src="<?= $ti['src'] ?>" alt="" /></span>
|
||||
<span class="tab-text"><?= $ti['code_name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 탭선택에 따른 동적 컨텐츠영역 -->
|
||||
<ul class="video-grid" id="video-list"></ul>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
let currentText = $(this).find('.tab-text').text();
|
||||
console.log('tabs');
|
||||
console.log(currentText);
|
||||
$('.current').text(currentText);
|
||||
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
/*
|
||||
let currentText = $('.current').find('.tab-text').text();
|
||||
if(currentText==""){$('.current').text("리더십 입문");}
|
||||
*/
|
||||
let firstTabText = $('.leadership-tab.is-active').find('.tab-text').text();
|
||||
if($('.current').text() == "" && firstTabText != ""){
|
||||
$('.current').text(firstTabText);
|
||||
}
|
||||
});
|
||||
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
//let category = $('.leadership-tab.is-active').data('cate') || 'CA200L01';
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var urlCate = urlParams.get('cate') || '';
|
||||
if (urlCate) {
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
var $matchTab = $('.leadership-tab[data-cate="' + urlCate + '"]');
|
||||
if ($matchTab.length) {
|
||||
$matchTab.addClass('is-active').attr('aria-selected', 'true');
|
||||
}
|
||||
}
|
||||
let category = urlCate || $('.leadership-tab.is-active').data('cate') || 'CA200<?= $SET_PREFIX ?>01';
|
||||
let sort = $('.select-sort').val() || 'latest';
|
||||
|
||||
loadVideos();
|
||||
|
||||
$('.leadership-tab').on('click', function () {
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.select-sort').on('change', function () {
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.container').on('scroll', function () {
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = this.scrollTop;
|
||||
const windowHeight = this.clientHeight;
|
||||
const docHeight = this.scrollHeight;
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 100) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('.container').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/ajax/get_video_list.php?SET_PREFIX=<?= $SET_PREFIX ?>',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
if (currentRequestSeq !== requestSeq) return;
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) page--;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) page--;
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- 영상 모달 댓글 스타일 (온보딩 동일) -->
|
||||
<!-- ============================================ -->
|
||||
<style>
|
||||
/* #leadershipVideoModal .comment-list { list-style: none; margin: 0; padding: 0; }
|
||||
#leadershipVideoModal .comment-list li { padding: 0; border-bottom: none; }
|
||||
#leadershipVideoModal .comment-list li.empty-comment { padding: 20px; text-align: center; color: rgba(255,255,255,0.4); font-size: 13px; }
|
||||
#leadershipVideoModal .comment-list .comment-info { display: flex; align-items: flex-start; }
|
||||
#leadershipVideoModal .comment-list .comment-info .photo { flex-shrink: 0; margin-right: 6px; display: flex; align-items: center; }
|
||||
#leadershipVideoModal .comment-list .comment-info .photo img { width: 20px; height: 20px; border-radius: 50%; }
|
||||
#leadershipVideoModal .comment-list .comment-info .user-comment { flex: 1; min-width: 0; display: flex; align-items: center; }
|
||||
#leadershipVideoModal .comment-list .comment-info .user-comment .user-name { font-size: 13px; color: #ccc; white-space: nowrap; flex-shrink: 0; margin-right: 8px; }
|
||||
#leadershipVideoModal .comment-list .comment-info .user-comment .user-text { flex: 1; background: transparent; border: none; color: #fff; font-size: 13px; line-height: 1.4; resize: none; padding: 10px 0 0 0; word-break: break-word; font-family: inherit; }
|
||||
#leadershipVideoModal .comment-list .comment-info .user-comment .user-text:disabled { opacity: 1; cursor: default; padding-top: 10px;}
|
||||
#leadershipVideoModal .comment-list .comment-info .user-comment .user-text:not(:disabled) { border: 1px solid #555; border-radius: 4px; padding: 4px 8px; background: #2a2a2a; }
|
||||
#leadershipVideoModal .comment-list .comment-info .actions { display: flex; gap: 4px; align-items: center; flex-shrink: 0; margin-left: 8px; align-self: center; }
|
||||
#leadershipVideoModal .comment-list .comment-info .actions button { padding: 3px 8px; font-size: 12px; border: 1px solid #555; border-radius: 3px; background: #333; color: #ddd; cursor: pointer; white-space: nowrap; }
|
||||
#leadershipVideoModal .comment-list .comment-info .actions button:hover { background: #555; color: #fff; }
|
||||
#leadershipVideoModal .comment-list li.editing .user-text { border: 1px solid #00ffcc !important; } */
|
||||
</style>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- 영상 모달 (video.php 구조 참고) -->
|
||||
<!-- ============================================ -->
|
||||
<div class="modal video" id="leadershipVideoModal" style="display:none">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="video-contents">
|
||||
<div class="video-area">
|
||||
<div class="video-box">
|
||||
<div id="videoPlayer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="tit-box">
|
||||
<div class="meta">
|
||||
<span id="modalCategoryName"></span>
|
||||
<em id="modalSubCategory"></em>
|
||||
</div>
|
||||
<h3 id="modalTitle"></h3>
|
||||
<label class="bookmark" for="leadership-bookmark-chk">
|
||||
<input id="leadership-bookmark-chk" type="checkbox" value="" />
|
||||
<span>북마크</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="desc" id="modalDesc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-side">
|
||||
<div class="video-header">
|
||||
<h5 class="tit">관련영상</h5>
|
||||
<!-- <span class="badge" id="modalBadge">리더십</span> -->
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<div class="video-list">
|
||||
<ul id="recommendedList">
|
||||
<!-- JS로 동적 렌더링 -->
|
||||
</ul>
|
||||
</div>
|
||||
<div class="comment-wrap">
|
||||
<!-- 댓글 목록 -->
|
||||
<div class="comment-list-wrap">
|
||||
<ul class="comment-list">
|
||||
<li class="empty-comment" style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">작성된 댓글이 없습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 댓글 입력 -->
|
||||
<div class="comment-box">
|
||||
<textarea placeholder="댓글을 작성해주세요"></textarea>
|
||||
<div class="btn-area">
|
||||
<button class="btn-cancel" disabled>취소</button>
|
||||
<button class="btn-save" disabled>등록</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- 영상 모달 JS (클릭 → 모달 열기 / 추천영상 / 댓글) -->
|
||||
<!-- ============================================ -->
|
||||
<script>
|
||||
$(function () {
|
||||
var API_BASE = '/bbs/api';
|
||||
var $modal = $('#leadershipVideoModal');
|
||||
var currentContentId = null;
|
||||
var editingCommentId = null;
|
||||
|
||||
// ── YouTube IFrame API 시청 추적 변수 ──
|
||||
var ytPlayer = null;
|
||||
var trackingInterval = null;
|
||||
var lastSaveTime = 0;
|
||||
var lastCurrentTime = 0;
|
||||
|
||||
// YouTube IFrame API 스크립트 로드
|
||||
if (!document.getElementById('youtube-iframe-api')) {
|
||||
var ytScript = document.createElement('script');
|
||||
ytScript.id = 'youtube-iframe-api';
|
||||
ytScript.src = 'https://www.youtube.com/iframe_api';
|
||||
ytScript.async = true;
|
||||
document.head.appendChild(ytScript);
|
||||
}
|
||||
|
||||
// ── YouTube 비디오 ID 추출 ──
|
||||
function extractVideoId(url) {
|
||||
if (!url) return '';
|
||||
var videoId = url;
|
||||
if (url.indexOf('youtube.com/watch') > -1) {
|
||||
try { videoId = new URL(url).searchParams.get('v') || url; } catch (e) {}
|
||||
} else if (url.indexOf('youtu.be/') > -1) {
|
||||
videoId = url.split('youtu.be/')[1].split(/[?&#]/)[0];
|
||||
} else if (url.indexOf('youtube.com/embed/') > -1) {
|
||||
videoId = url.split('youtube.com/embed/')[1].split(/[?&#]/)[0];
|
||||
}
|
||||
if (videoId && videoId.indexOf('/') === -1 && videoId.indexOf('.') === -1) {
|
||||
return videoId;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── YT.Player 생성 ──
|
||||
function initYTPlayer(videoId, startSeconds) {
|
||||
destroyYTPlayer();
|
||||
startSeconds = Math.floor(startSeconds || 0);
|
||||
|
||||
var createPlayer = function () {
|
||||
ytPlayer = new YT.Player('videoPlayer', {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
videoId: videoId,
|
||||
playerVars: {
|
||||
autoplay: 1,
|
||||
controls: 1,
|
||||
rel: 0,
|
||||
modestbranding: 1,
|
||||
enablejsapi: 1,
|
||||
start: startSeconds
|
||||
},
|
||||
events: {
|
||||
'onReady': function (event) {
|
||||
if (event.target && typeof event.target.playVideo === 'function') {
|
||||
event.target.playVideo();
|
||||
}
|
||||
},
|
||||
'onStateChange': function (event) {
|
||||
handlePlayerStateChange(event);
|
||||
},
|
||||
'onError': function (event) {
|
||||
console.error('[Leadership] Player Error:', event.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
|
||||
createPlayer();
|
||||
} else {
|
||||
// YT API 아직 로드 안 됨 → 대기
|
||||
var checkYt = setInterval(function () {
|
||||
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
|
||||
clearInterval(checkYt);
|
||||
createPlayer();
|
||||
}
|
||||
}, 500);
|
||||
setTimeout(function () { clearInterval(checkYt); }, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 플레이어 파괴 ──
|
||||
function destroyYTPlayer() {
|
||||
stopTracking();
|
||||
if (ytPlayer && typeof ytPlayer.destroy === 'function') {
|
||||
try { ytPlayer.destroy(); } catch (e) {}
|
||||
}
|
||||
ytPlayer = null;
|
||||
// YT.Player가 div를 iframe으로 교체하므로 다시 div를 복원
|
||||
var $box = $modal.find('.video-box');
|
||||
if (!$box.find('#videoPlayer').length) {
|
||||
$box.html('<div id="videoPlayer"></div>');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 플레이어 상태 변경 핸들러 (VideoModalBase 패턴) ──
|
||||
function handlePlayerStateChange(event) {
|
||||
if (event.data === YT.PlayerState.PLAYING) {
|
||||
startTracking();
|
||||
} else {
|
||||
stopTracking();
|
||||
if (event.data === YT.PlayerState.PAUSED || event.data === YT.PlayerState.ENDED) {
|
||||
saveLearningProgress(Math.floor(lastSaveTime), 'N');
|
||||
lastSaveTime = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 시청 추적 시작 (1초 주기) ──
|
||||
function startTracking() {
|
||||
if (trackingInterval) return;
|
||||
lastSaveTime = 0;
|
||||
|
||||
trackingInterval = setInterval(function () {
|
||||
if (!ytPlayer || typeof ytPlayer.getCurrentTime !== 'function') return;
|
||||
|
||||
var playerState = (typeof ytPlayer.getPlayerState === 'function')
|
||||
? ytPlayer.getPlayerState() : null;
|
||||
if (playerState !== YT.PlayerState.PLAYING) return;
|
||||
|
||||
var currentTime = ytPlayer.getCurrentTime();
|
||||
var diff = currentTime - lastCurrentTime;
|
||||
if (Math.abs(diff) <= 5) {
|
||||
lastSaveTime += 1;
|
||||
}
|
||||
lastCurrentTime = currentTime;
|
||||
|
||||
// 5초마다 서버에 heartbeat 전송
|
||||
if (lastSaveTime >= 5) {
|
||||
saveLearningProgress(lastSaveTime);
|
||||
lastSaveTime = 0;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ── 시청 추적 중지 ──
|
||||
function stopTracking() {
|
||||
if (trackingInterval) {
|
||||
clearInterval(trackingInterval);
|
||||
trackingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 서버에 학습 진행상황 저장 (save_learning.php) ──
|
||||
function saveLearningProgress(heartbeatSeconds, isWatching) {
|
||||
if (!ytPlayer || !currentContentId) return;
|
||||
if (typeof ytPlayer.getCurrentTime !== 'function') return;
|
||||
|
||||
var currentTime = Math.floor(ytPlayer.getCurrentTime());
|
||||
var duration = Math.floor(ytPlayer.getDuration());
|
||||
var incrementalWatch = Math.max(0, Math.floor(heartbeatSeconds || 0));
|
||||
|
||||
var params = new URLSearchParams({
|
||||
content_id: String(currentContentId),
|
||||
watch_tm: String(Math.max(0, currentTime)),
|
||||
content_tm: String(Math.max(0, duration)),
|
||||
all_tm_increment: String(incrementalWatch),
|
||||
is_watching: isWatching === 'N' ? 'N' : 'Y'
|
||||
});
|
||||
|
||||
fetch(API_BASE + '/save_learning.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: params.toString(),
|
||||
keepalive: true
|
||||
}).then(function (r) { return r.json(); }).then(function (result) {
|
||||
if (!result.success) {
|
||||
console.error('[Leadership] Save Learning Failed:', result.message);
|
||||
}
|
||||
}).catch(function (e) {
|
||||
console.error('[Leadership] Save Learning Error:', e);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 영상 카드 클릭 → 모달 열기 ──
|
||||
$(document).on('click', '#video-list .video-item .card-link', function (e) {
|
||||
e.preventDefault();
|
||||
var $item = $(this).closest('.video-item');
|
||||
|
||||
var contentId = $item.data('content-id') || $item.attr('data-content-id') || '';
|
||||
var videoUrl = $item.data('content-url') || $item.attr('data-content-url') || '';
|
||||
var title = $item.data('title') || $item.find('.item-title').text().trim() || '';
|
||||
var desc = $item.data('description') || '';
|
||||
var catName = $item.data('category-name') || $item.find('.item-category').text().trim() || '리더십';
|
||||
var subCat = $item.data('sub-category') || '';
|
||||
var watchTm = parseInt($item.data('watch-tm') || $item.attr('data-watch-tm') || '0', 10);
|
||||
var contentTm = parseInt($item.data('content-tm') || $item.attr('data-content-tm') || '0', 10);
|
||||
var isBookmarked = $item.find('.bookmark input[type="checkbox"]').is(':checked');
|
||||
|
||||
if (!contentId) {
|
||||
console.warn('[VideoModal] content-id가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
openModal({
|
||||
contentId: contentId,
|
||||
videoUrl: videoUrl,
|
||||
title: title,
|
||||
description: desc,
|
||||
categoryName: catName,
|
||||
subCategory: subCat,
|
||||
watchTm: watchTm,
|
||||
contentTm: contentTm,
|
||||
bookmark: isBookmarked
|
||||
});
|
||||
});
|
||||
|
||||
// ── 모달 열기 ──
|
||||
function openModal(info) {
|
||||
currentContentId = info.contentId;
|
||||
editingCommentId = null;
|
||||
lastSaveTime = 0;
|
||||
lastCurrentTime = Math.floor(info.watchTm || 0);
|
||||
|
||||
// YouTube IFrame API 플레이어 세팅
|
||||
var videoId = extractVideoId(info.videoUrl);
|
||||
if (videoId) {
|
||||
initYTPlayer(videoId, info.watchTm || 0);
|
||||
}
|
||||
|
||||
// 서버에서 최신 watch_tm 조회 후 재생 위치 보정
|
||||
$.ajax({
|
||||
url: '/bbs/myclass/api/get_video_time.php',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ content_id: info.contentId }),
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.success && res.watch_tm > 0) {
|
||||
var serverWatchTm = parseInt(res.watch_tm, 10);
|
||||
if (ytPlayer && typeof ytPlayer.seekTo === 'function' && serverWatchTm > (info.watchTm || 0)) {
|
||||
ytPlayer.seekTo(serverWatchTm, true);
|
||||
lastCurrentTime = serverWatchTm;
|
||||
}
|
||||
// 카드 data-watch-tm 동기화
|
||||
$('#video-list .video-item[data-content-id="' + info.contentId + '"]').attr('data-watch-tm', serverWatchTm);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 영상 정보
|
||||
$modal.find('#modalTitle').text(info.title);
|
||||
// 설명: 줄바꿈을 <br>로 변환하여 표시
|
||||
var descHtml = $('<div>').text(info.description || '').html().replace(/\n/g, '<br>');
|
||||
$modal.find('#modalDesc').html(descHtml);
|
||||
$modal.find('#modalCategoryName').text(info.categoryName || '리더십');
|
||||
$modal.find('#modalSubCategory').text(info.subCategory || '');
|
||||
$modal.find('#modalBadge').text(info.categoryName || '리더십');
|
||||
|
||||
// 추천영상 리스트
|
||||
buildRecommendedList(info.contentId);
|
||||
|
||||
// 댓글 로드
|
||||
loadComments(info.contentId);
|
||||
resetCommentForm();
|
||||
|
||||
// 북마크 상태 동기화
|
||||
syncBookmark(info.contentId, info.bookmark);
|
||||
|
||||
// 모달 표시
|
||||
$modal.show();
|
||||
$('body').css('overflow', 'hidden');
|
||||
}
|
||||
|
||||
// ── 모달 닫기 (진행상황 저장 후 닫기) ──
|
||||
function closeModal() {
|
||||
// 마지막 시청 진행상황 저장
|
||||
stopTracking();
|
||||
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
|
||||
var currentTime = Math.floor(ytPlayer.getCurrentTime());
|
||||
// 카드의 data-watch-tm 업데이트 (재오픈 시 적용)
|
||||
$('#video-list .video-item[data-content-id="' + currentContentId + '"]').attr('data-watch-tm', currentTime);
|
||||
saveLearningProgress(Math.floor(lastSaveTime), 'N');
|
||||
}
|
||||
lastSaveTime = 0;
|
||||
|
||||
destroyYTPlayer();
|
||||
$modal.hide();
|
||||
$('body').css('overflow', '');
|
||||
currentContentId = null;
|
||||
editingCommentId = null;
|
||||
}
|
||||
|
||||
$modal.on('click', '.close', closeModal);
|
||||
$modal.on('click', function (e) {
|
||||
if ($(e.target).hasClass('modal')) closeModal();
|
||||
});
|
||||
$(document).on('keydown', function (e) {
|
||||
if (e.key === 'Escape' && $modal.is(':visible')) closeModal();
|
||||
});
|
||||
|
||||
// ── 북마크 동기화 ──
|
||||
function syncBookmark(contentId, isBookmarked) {
|
||||
var $chk = $modal.find('.bookmark input[type="checkbox"]');
|
||||
if (!$chk.length) return;
|
||||
if (isBookmarked !== undefined) {
|
||||
$chk.prop('checked', !!isBookmarked);
|
||||
} else {
|
||||
// 서버에서 북마크 상태 조회
|
||||
$.ajax({
|
||||
url: '/bbs/myclass/api/get_video_time.php',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ content_id: contentId }),
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.success) {
|
||||
$chk.prop('checked', !!res.is_bookmarked);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 북마크 토글 이벤트 ──
|
||||
$modal.on('change', '.bookmark input[type="checkbox"]', function () {
|
||||
var $chk = $(this);
|
||||
var isActive = $chk.is(':checked') ? '1' : '0';
|
||||
if (!currentContentId) return;
|
||||
$chk.prop('disabled', true);
|
||||
$.ajax({
|
||||
url: '/bbs/api/save_wishlist.php',
|
||||
type: 'POST',
|
||||
data: { content_id: String(currentContentId), is_active: isActive },
|
||||
dataType: 'json',
|
||||
success: function (res) {
|
||||
if (!res || res.success !== true) {
|
||||
$chk.prop('checked', !$chk.is(':checked'));
|
||||
alert(res?.message || '북마크 저장에 실패했습니다.');
|
||||
} else {
|
||||
// 목록 카드의 북마크 상태도 동기화
|
||||
var $card = $('#video-list .video-item[data-content-id="' + currentContentId + '"] .bookmark input');
|
||||
$card.prop('checked', isActive === '1');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$chk.prop('checked', !$chk.is(':checked'));
|
||||
alert('북마크 저장 중 오류가 발생했습니다.');
|
||||
},
|
||||
complete: function () {
|
||||
$chk.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── 추천영상 리스트 구성 (키워드 기반 API) ──
|
||||
function buildRecommendedList(contentId) {
|
||||
var $list = $modal.find('#recommendedList');
|
||||
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">추천 영상 로딩 중...</li>');
|
||||
|
||||
if (!contentId) {
|
||||
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
|
||||
return;
|
||||
}
|
||||
|
||||
$.getJSON(API_BASE + '/get_recommend_videos.php', { content_id: contentId })
|
||||
.done(function (data) {
|
||||
if (!data.success || !data.videos || data.videos.length === 0) {
|
||||
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
|
||||
// 키워드 배지 숨기기
|
||||
$modal.find('#modalBadge').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// 키워드 배지 업데이트
|
||||
if (data.keywords && data.keywords.length > 0) {
|
||||
$modal.find('#modalBadge').text(data.keywords.join(', ')).show();
|
||||
} else {
|
||||
$modal.find('#modalBadge').hide();
|
||||
}
|
||||
|
||||
$list.empty();
|
||||
var catClassMap = {
|
||||
'CA10001': 'myclass', 'CA10002': 'onboarding', 'CA10003': 'legal',
|
||||
'CA10004': 'leader', 'CA10005': 'insight', 'CA10006': 'biztrend'
|
||||
};
|
||||
|
||||
data.videos.forEach(function (v) {
|
||||
var safeTitle = $('<div>').text(v.title || '').html();
|
||||
var safeCat = $('<div>').text(v.category_name || '').html();
|
||||
var thumb = v.thumbnail || '/img/video/img_thumb_01.png';
|
||||
var catClass = catClassMap[v.category_code] || '';
|
||||
|
||||
var li =
|
||||
'<li>' +
|
||||
'<a href="#" class="list" ' +
|
||||
'data-content-id="' + (v.content_id || '') + '" ' +
|
||||
'data-content-url="' + (v.content_url || '').replace(/"/g, '"') + '" ' +
|
||||
'data-title="' + (v.title || '').replace(/"/g, '"') + '" ' +
|
||||
'data-description="' + (v.description || '').replace(/"/g, '"') + '" ' +
|
||||
'data-category-name="' + (v.category_name || '').replace(/"/g, '"') + '">' +
|
||||
'<div class="thumb"><img src="' + thumb + '" alt="" /></div>' +
|
||||
'<div class="txt-box">' +
|
||||
'<div class="category ' + catClass + '">' + safeCat + '</div>' +
|
||||
'<div class="title">' + safeTitle + '</div>' +
|
||||
'</div>' +
|
||||
'</a>' +
|
||||
'</li>';
|
||||
$list.append(li);
|
||||
});
|
||||
})
|
||||
.fail(function () {
|
||||
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
|
||||
});
|
||||
}
|
||||
|
||||
// ── 추천영상 클릭 → 영상 전환 ──
|
||||
$modal.on('click', '#recommendedList .list', function (e) {
|
||||
e.preventDefault();
|
||||
var $a = $(this);
|
||||
// 현재 영상 진행상황 저장 후 전환
|
||||
stopTracking();
|
||||
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
|
||||
saveLearningProgress(Math.floor(lastSaveTime), 'N');
|
||||
}
|
||||
lastSaveTime = 0;
|
||||
openModal({
|
||||
contentId: $a.data('content-id'),
|
||||
videoUrl: $a.data('content-url') || '',
|
||||
title: $a.data('title') || $a.find('.title').text().trim(),
|
||||
description: $a.data('description') || '',
|
||||
categoryName: $a.data('category-name') || '리더십'
|
||||
});
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// 댓글 CRUD
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// ── 댓글 목록 로드 ──
|
||||
function loadComments(contentId) {
|
||||
var $commentList = $modal.find('.comment-list');
|
||||
if (!contentId) return;
|
||||
|
||||
$.getJSON(API_BASE + '/get_comments.php', { content_id: contentId })
|
||||
.done(function (data) {
|
||||
if (data.success && Array.isArray(data.data)) {
|
||||
renderComments(data.data);
|
||||
} else {
|
||||
$commentList.html('<li class="empty">등록된 댓글이 없습니다.</li>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$commentList.html('<li class="empty">댓글을 불러올 수 없습니다.</li>');
|
||||
});
|
||||
}
|
||||
|
||||
// ── 댓글 렌더링 (온보딩 VideoModalBase 동일 구조) ──
|
||||
function renderComments(comments) {
|
||||
var $commentList = $modal.find('.comment-list');
|
||||
if (!comments.length) {
|
||||
$commentList.html('<li class="empty-comment" style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">작성된 댓글이 없습니다.</li>');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = comments.map(function (c) {
|
||||
var safeComment = (c.comment || '')
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
var safeName = (c.member_name || '익명')
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
var actions = c.is_author
|
||||
? '<div class="actions">' +
|
||||
'<button type="button" class="btn-edit-comment" data-id="' + c.id + '">수정</button>' +
|
||||
'<button type="button" class="btn-del-comment" data-id="' + c.id + '">삭제</button>' +
|
||||
'</div>'
|
||||
: '';
|
||||
|
||||
return '<li data-id="' + c.id + '">' +
|
||||
'<div class="comment-info">' +
|
||||
'<div class="photo"><img src="/img/ico/ico_user.svg" /></div>' +
|
||||
'<div class="user-comment">' +
|
||||
'<span class="user-name">' + safeName + '</span>' +
|
||||
'<textarea class="user-text" disabled>' + safeComment + '</textarea>' +
|
||||
'</div>' +
|
||||
actions +
|
||||
'</div>' +
|
||||
'</li>';
|
||||
}).join('');
|
||||
|
||||
$commentList.html(html);
|
||||
adjustUserTextHeights();
|
||||
}
|
||||
|
||||
// ── 댓글 textarea 높이 자동 조정 ──
|
||||
function adjustUserTextHeights() {
|
||||
$modal.find('.user-text').each(function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = this.scrollHeight + 'px';
|
||||
this.style.overflow = 'hidden';
|
||||
});
|
||||
}
|
||||
|
||||
// ── 댓글 인라인 수정 (온보딩 동일) ──
|
||||
$modal.on('click', '.btn-edit-comment', function (e) {
|
||||
e.preventDefault();
|
||||
var $li = $(this).closest('li');
|
||||
var $textarea = $li.find('.user-text');
|
||||
var $btn = $(this);
|
||||
|
||||
if ($btn.text() === '수정') {
|
||||
$textarea.prop('disabled', false).focus();
|
||||
$btn.text('저장').css('color', '#00ffcc');
|
||||
$li.addClass('editing');
|
||||
} else {
|
||||
var newComment = $textarea.val().trim();
|
||||
if (!newComment) return;
|
||||
var commentId = $btn.data('id');
|
||||
|
||||
$.ajax({
|
||||
url: API_BASE + '/save_comment.php',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ content_id: currentContentId, comment: newComment, id: commentId }),
|
||||
success: function (data) {
|
||||
if (data.success) {
|
||||
$btn.text('수정').css('color', '');
|
||||
$textarea.prop('disabled', true);
|
||||
$li.removeClass('editing');
|
||||
loadComments(currentContentId);
|
||||
} else {
|
||||
alert(data.message || '저장에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
error: function () { alert('저장 중 오류가 발생했습니다.'); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ── 댓글 삭제 (온보딩 동일) ──
|
||||
$modal.on('click', '.btn-del-comment', function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
var commentId = $(this).data('id');
|
||||
|
||||
$.ajax({
|
||||
url: API_BASE + '/delete_comment.php',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ id: commentId }),
|
||||
success: function (data) {
|
||||
if (data.success) {
|
||||
loadComments(currentContentId);
|
||||
} else {
|
||||
alert(data.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
error: function () { alert('삭제 중 오류가 발생했습니다.'); }
|
||||
});
|
||||
});
|
||||
|
||||
// ── 새 댓글 작성 ──
|
||||
$modal.on('click', '.comment-box .btn-save', function () {
|
||||
var $textarea = $modal.find('.comment-box textarea');
|
||||
var comment = $textarea.val().trim();
|
||||
if (!comment || !currentContentId) return;
|
||||
|
||||
var payload = { content_id: currentContentId, comment: comment };
|
||||
|
||||
$.ajax({
|
||||
url: API_BASE + '/save_comment.php',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload),
|
||||
success: function (data) {
|
||||
if (data.success) {
|
||||
resetCommentForm();
|
||||
loadComments(currentContentId);
|
||||
} else {
|
||||
alert(data.message || '저장에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
error: function () { alert('저장 중 오류가 발생했습니다.'); }
|
||||
});
|
||||
});
|
||||
|
||||
// ── 댓글 취소 ──
|
||||
$modal.on('click', '.comment-box .btn-cancel', function () {
|
||||
resetCommentForm();
|
||||
});
|
||||
|
||||
// ── textarea 입력 → 버튼 활성화 ──
|
||||
$modal.on('input', '.comment-box textarea', function () {
|
||||
var hasText = $(this).val().trim().length > 0;
|
||||
$modal.find('.btn-cancel').prop('disabled', !hasText);
|
||||
$modal.find('.btn-save').prop('disabled', !hasText);
|
||||
});
|
||||
|
||||
function resetCommentForm() {
|
||||
$modal.find('.comment-box textarea').val('');
|
||||
$modal.find('.btn-cancel').prop('disabled', true);
|
||||
$modal.find('.btn-save').prop('disabled', true).text('등록');
|
||||
editingCommentId = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,388 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./leadership.html">리더십</a></li>
|
||||
<li><span class="current">셀프 리더십</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_01_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_01.png" alt="실천으로 완성하는 리더십">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_02_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_02.png" alt="성장하는 리더십 여정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_03.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_03_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_03.png" alt="리더로 성장하는 과정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="리더십 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_01.svg" alt="" /></span>
|
||||
<span class="tab-text">리더십 입문111</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_02.svg" alt="" /></span>
|
||||
<span class="tab-text">셀프리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_03.svg" alt="" /></span>
|
||||
<span class="tab-text">팀리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_04.svg" alt="" /></span>
|
||||
<span class="tab-text">실전 리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_05.svg" alt="" /></span>
|
||||
<span class="tab-text">인물탐구</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list" aria-label="리더십 콘텐츠 목록">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em>8</em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option selected>조회수</option>
|
||||
<option>업데이트</option>
|
||||
<option>내가본컨텐츠</option>
|
||||
<option>안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="video-grid">
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l1" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l1" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_01.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">목표관리, 성과관리의 차이? 더 중요한 것은?</strong>
|
||||
<span class="item-desc">리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l2" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l2" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_02.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l3" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l3" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_03.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">경제경영이론을 이용한 동기부여 이론들</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l4" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">DT시대의 애자일 경영, 비즈니스 어질리티</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l1" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l1" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_01.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">목표관리, 성과관리의 차이? 더 중요한 것은?</strong>
|
||||
<span class="item-desc">리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l2" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l2" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_02.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l3" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l3" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_03.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">경제경영이론을 이용한 동기부여 이론들</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l4" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">DT시대의 애자일 경영, 비즈니스 어질리티</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l1" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l1" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_01.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">목표관리, 성과관리의 차이? 더 중요한 것은?</strong>
|
||||
<span class="item-desc">리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l2" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l2" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_02.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">[경영 추천도서] 팀장이 처음이신가요? | 팀장 리더십 수업</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l3" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l3" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_03.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">경제경영이론을 이용한 동기부여 이론들</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l4" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l4" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">DT시대의 애자일 경영, 비즈니스 어질리티</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l5" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l5" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_05.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">조직문화가 저희의 가장 강력한 무기입니다...</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l6" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l6" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_06.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">꼰대가 되지 않고 건설적인 피드백을 하는 법</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l7" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l7" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_07.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">제대로 된 평가 면담, 어떻게 할 수 있을까? (SAS, 듀폰)</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link">
|
||||
<label class="bookmark" for="like_chk_l8" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" id="like_chk_l8" title="좋아요" />
|
||||
</label>
|
||||
<div class="item-thumb">
|
||||
<img src="/edu/img/video/img_thumb_04.png" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title">성장 마인드셋 실패에서 배우기</strong>
|
||||
<span class="item-desc">셀프리더십</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,334 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./leadership.html">리더십</a></li>
|
||||
<li><span class="current">셀프 리더십</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_01_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_01.png" alt="실천으로 완성하는 리더십">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_02_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_02.png" alt="성장하는 리더십 여정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_03.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_03_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_03.png" alt="리더로 성장하는 과정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="리더십 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L01">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_01.svg" alt="" /></span>
|
||||
<span class="tab-text">리더십 입문</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true" data-cate="CA200L02">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_02.svg" alt="" /></span>
|
||||
<span class="tab-text">셀프리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L03">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_03.svg" alt="" /></span>
|
||||
<span class="tab-text">팀리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L04">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_04.svg" alt="" /></span>
|
||||
<span class="tab-text">실전 리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L05">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_05.svg" alt="" /></span>
|
||||
<span class="tab-text">인물탐구</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list" aria-label="리더십 콘텐츠 목록">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="video-grid" id="video-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
let category = $('.leadership-tab.is-active').data('cate') || 'CA200L02';
|
||||
let sort = $('.select-sort').val() || 'view';
|
||||
|
||||
loadVideos();
|
||||
|
||||
/* 탭 클릭 */
|
||||
$('.leadership-tab').on('click', function () {
|
||||
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
/* 정렬 변경 */
|
||||
$('.select-sort').on('change', function () {
|
||||
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
console.log('scrollTop='+scrollTop);
|
||||
console.log('windowHeight='+windowHeight);
|
||||
console.log('docHeight='+docHeight);
|
||||
|
||||
console.log('scrollTop + windowHeight=');
|
||||
console.log(scrollTop + windowHeight);
|
||||
|
||||
console.log('docHeight - 400=');
|
||||
console.log(docHeight - 450);
|
||||
|
||||
|
||||
|
||||
$(window).on('scroll', function () {
|
||||
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = $(window).scrollTop();
|
||||
const windowHeight = $(window).height();
|
||||
const docHeight = $(document).height();
|
||||
|
||||
|
||||
|
||||
.container
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* 무한스크롤 */
|
||||
$(window).on('scroll', function () {
|
||||
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = $(window).scrollTop();
|
||||
const windowHeight = $(window).height();
|
||||
const docHeight = $(document).height();
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 200) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('html, body').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
|
||||
if (isLoading) {
|
||||
$('#video-loading').show();
|
||||
} else {
|
||||
$('#video-loading').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/edu/ajax/get_video_list.php',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
|
||||
/* 이전 요청이 늦게 도착한 경우 무시 */
|
||||
if (currentRequestSeq !== requestSeq) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) {
|
||||
page--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
|
||||
if (page === 1 && html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) {
|
||||
page--;
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap leadership">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<!-- editor's pick -->
|
||||
<section class="leadership-hero">
|
||||
<div class="leadership-inner hero-top hero-breadcrumb">
|
||||
<ul class="breadcrumb" aria-label="breadcrumb">
|
||||
<li><a href="./index.html">홈</a></li>
|
||||
<li><a href="./leadership.html">리더십</a></li>
|
||||
<li><span class="current">셀프 리더십</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- editor's pick -->
|
||||
|
||||
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
|
||||
<div class="hero-banner">
|
||||
<div class="swiper hero-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_01.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_01_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_01.png" alt="실천으로 완성하는 리더십">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_02.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_02_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_02.png" alt="성장하는 리더십 여정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swiper-slide">
|
||||
<div class="banner-img">
|
||||
<picture>
|
||||
<source media="(min-width: 769px)" srcset="/edu/img/leadership/img_banner_03.png">
|
||||
<source media="(max-width: 768px)" srcset="/edu/img/leadership/img_banner_03_m.png">
|
||||
<img src="/edu/img/leadership/img_banner_03.png" alt="리더로 성장하는 과정">
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="leadership-tabs" aria-label="리더십 카테고리">
|
||||
<div class="leadership-inner">
|
||||
<div class="leadership-tabs-list" role="tablist" aria-label="리더십 카테고리 탭">
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L01">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_01.svg" alt="" /></span>
|
||||
<span class="tab-text">리더십 입문</span>
|
||||
</button>
|
||||
<button class="leadership-tab is-active" type="button" role="tab" aria-selected="true" data-cate="CA200L02">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_02.svg" alt="" /></span>
|
||||
<span class="tab-text">셀프리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L03">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_03.svg" alt="" /></span>
|
||||
<span class="tab-text">팀리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L04">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_04.svg" alt="" /></span>
|
||||
<span class="tab-text">실전 리더십</span>
|
||||
</button>
|
||||
<button class="leadership-tab" type="button" role="tab" aria-selected="false" data-cate="CA200L05">
|
||||
<span class="tab-icon"><img src="/edu/img/ico/ico_leadership_05.svg" alt="" /></span>
|
||||
<span class="tab-text">인물탐구</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- video list -->
|
||||
|
||||
<section class="leadership-video-list" aria-label="리더십 콘텐츠 목록">
|
||||
<div class="leadership-inner">
|
||||
<div class="list-head">
|
||||
<span class="total">TOTAL <em></em></span>
|
||||
<div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option value="view" selected>조회수</option>
|
||||
<option value="latest" >업데이트</option>
|
||||
<option value="seen">내가본컨텐츠</option>
|
||||
<option value="unseen">안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="video-grid" id="video-list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- // container -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Hero Swiper
|
||||
new Swiper('.hero-swiper', {
|
||||
loop: true,
|
||||
autoplay: {
|
||||
delay: 4000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
speed: 600,
|
||||
pagination: {
|
||||
el: '.hero-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
tab.classList.add('is-active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
let page = 1;
|
||||
let loading = false;
|
||||
let lastPage = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
let category = $('.leadership-tab.is-active').data('cate') || 'CA200L02';
|
||||
let sort = $('.select-sort').val() || 'latest';
|
||||
|
||||
loadVideos();
|
||||
|
||||
$('.leadership-tab').on('click', function () {
|
||||
if (loading) return;
|
||||
|
||||
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
|
||||
$(this).addClass('is-active').attr('aria-selected', 'true');
|
||||
|
||||
category = $(this).data('cate');
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.select-sort').on('change', function () {
|
||||
if (loading) return;
|
||||
|
||||
sort = $(this).val();
|
||||
resetList(true);
|
||||
loadVideos();
|
||||
});
|
||||
|
||||
$('.container').on('scroll', function () {
|
||||
if (loading || lastPage) return;
|
||||
|
||||
const scrollTop = this.scrollTop;
|
||||
const windowHeight = this.clientHeight;
|
||||
const docHeight = this.scrollHeight;
|
||||
|
||||
if (scrollTop + windowHeight >= docHeight - 100) {
|
||||
page++;
|
||||
loadVideos();
|
||||
}
|
||||
});
|
||||
|
||||
function resetList(moveTop) {
|
||||
page = 1;
|
||||
lastPage = false;
|
||||
$('#video-list').empty();
|
||||
|
||||
if (moveTop) {
|
||||
$('.container').scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loading = isLoading;
|
||||
}
|
||||
|
||||
function loadVideos() {
|
||||
requestSeq++;
|
||||
const currentRequestSeq = requestSeq;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
$.ajax({
|
||||
url: '/edu/ajax/get_video_list.php',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: {
|
||||
category: category,
|
||||
sort: sort,
|
||||
page: page
|
||||
},
|
||||
success: function (res) {
|
||||
if (currentRequestSeq !== requestSeq) return;
|
||||
|
||||
if (!res || res.success !== true) {
|
||||
if (page > 1) page--;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = $.trim(res.html || '');
|
||||
const totalCount = parseInt(res.total_count, 10) || 0;
|
||||
|
||||
$('.total em').text(totalCount);
|
||||
|
||||
if (page === 1) {
|
||||
$('#video-list').html(html);
|
||||
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
$('#video-list').html(
|
||||
'<li class="video-item video-empty">' +
|
||||
'<div class="item-info">' +
|
||||
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
|
||||
'</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (html === '') {
|
||||
lastPage = true;
|
||||
page--;
|
||||
} else {
|
||||
$('#video-list').append(html);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (page > 1) page--;
|
||||
},
|
||||
complete: function () {
|
||||
if (currentRequestSeq === requestSeq) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/auth.php';
|
||||
edu_require_login();
|
||||
/**
|
||||
* skin/learning.php — 법정교육 페이지 뷰 템플릿
|
||||
*
|
||||
* bbs/legal_learning_data.php 가 제공하는 변수:
|
||||
* $userName - 사용자 이름
|
||||
* $userRank - 직위/직급
|
||||
* $dDay - D-day 숫자
|
||||
* $deadlineStr - 마감일 문자열
|
||||
* $chapters - 챕터 배열
|
||||
* $chaptersJson - JSON (window.learningChapterData)
|
||||
* $configJson - JSON (window.learningConfigData)
|
||||
* $progressRate - 전체 진도율 (0~100)
|
||||
*/
|
||||
require_once __DIR__ . '/../bbs/legal_learning_data.php';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap lessons">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="lessons-wrap">
|
||||
<div class="inner">
|
||||
<?php
|
||||
$today = new DateTime('today');
|
||||
$start = new DateTime('2026-06-08');
|
||||
$end = new DateTime('2026-06-30');
|
||||
|
||||
if ($today >= $start && $today <= $end) : ?>
|
||||
<a class="btn-edu-link" target="_blank" href="https://ehrd.kgeduone.co.kr/">
|
||||
<span>
|
||||
<em>산업안전보건 교육</em> <span>수강하기</span>
|
||||
</span>
|
||||
<i class="ico-outward"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="page-title">
|
||||
<h3><em><?= htmlspecialchars($userName) ?> <?= htmlspecialchars($userRank) ?>님</em></h3>
|
||||
<p>
|
||||
<em>D-<?= $dDay ?><small>(<?= $deadlineStr ?>)</small> 까지 필수 콘텐츠</em> 시청을
|
||||
완료해주세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="lessons-area">
|
||||
<div class="lessons-gauge">
|
||||
<!-- Start-line: 학습 목록이 있으면 자동으로 on 이미지로 변경됨 -->
|
||||
<div class="start-line">
|
||||
<img
|
||||
src="/img/learning/img_start_off.svg"
|
||||
alt="start"
|
||||
/>
|
||||
</div>
|
||||
<?php include(__DIR__ . "/learning_svg.php") ?>
|
||||
|
||||
<!-- 챕터 카드 컨테이너 (JavaScript에서 동적 생성) -->
|
||||
<ul class="chapter-list"></ul>
|
||||
<!-- 마커 컨테이너 -->
|
||||
<div id="markers-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 법정교육 데이터 (PHP → JS 주입) -->
|
||||
<script>
|
||||
// 챕터·레슨별 완료 상태 (gauge, markers, chapter-cards.js 에서 사용)
|
||||
window.learningConfigData = <?= $configJson ?>;
|
||||
|
||||
// 챕터 전체 데이터 (제목, 영상 URL, 진도 등)
|
||||
window.learningChapterData = <?= $chaptersJson ?>;
|
||||
|
||||
// 전체 진도율 (0~100)
|
||||
window.learningProgressRate = <?= $progressRate ?>;
|
||||
|
||||
// 디버그 정보 (development 모드)
|
||||
if (typeof window.learningDebugData === 'undefined') {
|
||||
window.learningDebugData = {};
|
||||
}
|
||||
window.learningDebugData.legal = <?= json_encode($debugData, JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
// 디버그 로그 출력
|
||||
if (window.learningChapterData && Array.isArray(window.learningChapterData) && window.learningChapterData.length === 0) {
|
||||
console.warn('[LearningPage] learningChapterData is EMPTY - using config.js fallback', window.learningDebugData.legal);
|
||||
} else if (window.learningChapterData && Array.isArray(window.learningChapterData)) {
|
||||
console.log('[LearningPage] learningChapterData loaded from DB:', window.learningChapterData.length, 'chapters');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- YouTube IFrame API (시간 스킵 제한에 필요) -->
|
||||
<script src="https://www.youtube.com/iframe_api"></script>
|
||||
<!-- 학습 페이지 스크립트 (의존성 순서 유지하며 defer로 비동기 로드) -->
|
||||
<script src="/js/learning/config.js" defer></script>
|
||||
<script src="/js/learning/gauge.js" defer></script>
|
||||
<script src="/js/learning/markers.js" defer></script>
|
||||
<script src="/js/learning/chapter-cards.js" defer></script>
|
||||
<script src="/js/learning/progress-indicator.js" defer></script>
|
||||
<script src="/js/learning/modal.js" defer></script>
|
||||
<script src="/js/learning/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap lessons">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="lessons-wrap">
|
||||
<div class="page-title">
|
||||
<h3><em>홍길동 선임연구원님</em></h3>
|
||||
<p>
|
||||
<em>D-66<small>(3월 20일)</small> 까지 필수 콘텐츠</em> 시청을
|
||||
완료해주세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="lessons-area">
|
||||
<div class="lessons-gauge">
|
||||
<!-- Start-line: 학습 목록이 있으면 자동으로 on 이미지로 변경됨 -->
|
||||
<div class="start-line">
|
||||
<img
|
||||
src="/img/learning/img_start_off.svg"
|
||||
alt="start"
|
||||
/>
|
||||
</div>
|
||||
<?php include(__DIR__ . "/learning_svg.php") ?>
|
||||
|
||||
<!-- 마커 컨테이너 -->
|
||||
<div id="markers-container"></div>
|
||||
|
||||
<!-- 챕터 카드 컨테이너 (JavaScript에서 동적 생성) -->
|
||||
<ul class="chapter-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTML에서 completed 값 설정 (config.js 로드 전에 설정) -->
|
||||
<script>
|
||||
|
||||
|
||||
// 예시: 챕터 1의 모든 레슨을 완료 상태로 설정
|
||||
window.learningConfigData = {
|
||||
1: {
|
||||
completed: true, // 챕터 1 자체 완료
|
||||
lessons: [
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
{ completed: true },
|
||||
],
|
||||
},
|
||||
2: {
|
||||
completed: true, // 챕터 2 자체 완료
|
||||
lessons: [{ completed: true }, { completed: true }],
|
||||
},
|
||||
4: {
|
||||
completed: true, // 챕터 3 자체 완료
|
||||
lessons: [{ completed: true }, { completed: true }],
|
||||
},
|
||||
5: {
|
||||
completed: true, // 챕터 3 자체 완료
|
||||
lessons: [{ completed: true }, { completed: true },{ completed: true }, { completed: true },{ completed: true }, { completed: true }],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- 학습 페이지 스크립트 (의존성 순서 유지하며 defer로 비동기 로드) -->
|
||||
<script src="/js/learning/config.js" defer></script>
|
||||
<script src="/js/learning/gauge.js" defer></script>
|
||||
<script src="/js/learning/markers.js" defer></script>
|
||||
<script src="/js/learning/chapter-cards.js" defer></script>
|
||||
<script src="/js/learning/progress-indicator.js" defer></script>
|
||||
<script src="/js/learning/modal.js" defer></script>
|
||||
<script src="/js/learning/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,327 @@
|
||||
<!-- PC용 게이지 SVG (768px 이상) -->
|
||||
<div class="gauge-svg-pc">
|
||||
<svg
|
||||
id="gauge-svg"
|
||||
width="1610"
|
||||
height="631"
|
||||
viewBox="0 0 1610 631"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0_14_397"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="25"
|
||||
y="0"
|
||||
width="1585"
|
||||
height="730"
|
||||
>
|
||||
<rect
|
||||
width="1584"
|
||||
height="730"
|
||||
transform="matrix(-1 0 0 1 1609.35 0)"
|
||||
fill="url(#paint0_linear_14_397)"
|
||||
/>
|
||||
</mask>
|
||||
<mask id="progressMask">
|
||||
<path
|
||||
id="maskPath"
|
||||
d="M -420.319 572.68 C -420.319 572.68 1411 648.6 1411 499.446 C 1408.18 305.242 118.94 387.337 118.94 290.767 C 118.94 183.271 1215.22 268.705 1215.22 185.273 C 1215.22 117.048 312.838 142.885 312.838 106.112 C 312.838 77.3325 719.914 71 719.914 71 H 720.826 C 720.826 71 317.003 79.7307 317.003 106.112"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="80"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="stroke-dasharray: 6050.94; stroke-dashoffset: 4840.75"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_14_397)">
|
||||
<g style="mix-blend-mode: multiply" opacity="0.2" filter="url(#filter0_f_14_397)">
|
||||
<path d="M352.858 106.774C352.858 83.6781 736.826 71 736.826 71H723.973C723.973 71 330.949 84.4519 330.949 107.94C330.949 156.604 1209.68 122.945 1205.11 191.15C1205.11 253.001 121.34 184.203 121.34 303.321C121.34 392.864 1393.35 333.757 1393.35 498.819C1393.35 593.455 -50.8243 581.056 -298.664 578.231C-320.836 577.201 -333.418 576.569 -334.563 576.511C-334.922 576.493 -334.649 576.808 -334.649 577.167C-334.649 577.51 -334.914 577.781 -334.571 577.786C-333.472 577.802 -320.87 577.978 -298.664 578.231C-45.2113 590.005 1461.35 653.777 1461.35 506.091C1461.35 311.639 164.09 374.692 164.09 298.323C164.09 188 1246.98 276.913 1246.98 188C1246.98 117.783 352.858 129.871 352.858 106.774Z" fill="url(#paint1_linear_14_397)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_iin_14_397)">
|
||||
<path d="M350.836 104.706C350.836 81.1939 733.748 72.9985 733.748 72.9985L733.734 70C733.734 70 330.58 81.9844 330.58 105.895C330.58 155.329 1214.5 123.065 1214.5 185.03C1214.5 246.994 117.771 173.59 117.771 294.228C117.771 408.603 1396.24 328.112 1396.24 496.29C1396.24 601.889 -349.649 573.741 -349.649 573.741L-338.498 577.159C-338.498 577.159 1464.35 665.697 1464.35 501.531C1464.35 303.579 164.313 369.848 164.313 292.634C164.313 191.663 1262.93 281.874 1262.93 181.829C1262.93 110.339 350.836 128.218 350.836 104.706Z" fill="url(#paint2_linear_14_397)"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_i_14_397)">
|
||||
<path id="path-bg" d="M342.354 106.078C342.354 79.7224 746.177 71 746.177 71H745.265C745.265 71 338.189 77.3265 338.189 106.078C338.189 142.816 1233.19 117.004 1233.19 185.164C1233.19 260.759 132.851 180.087 132.851 290.558C132.851 398.029 1418.02 312.02 1418.02 500.038C1418.02 623.271 -396.649 571.904 -396.649 571.904L-394.969 573.202C-394.969 573.202 1436.35 648.05 1436.35 499.038C1433.53 305.019 144.291 387.036 144.291 290.558C144.291 183.164 1240.57 268.517 1240.57 185.164C1240.57 117.221 342.354 137.505 342.354 106.078Z" fill="#CCBDA1"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_i_14_397)" mask="url(#progressMask)">
|
||||
<path id="path-fill" d="M342.354 106.112C342.354 79.7307 746.177 71 746.177 71H745.265C745.265 71 338.189 77.3325 338.189 106.112C338.189 142.885 1233.19 117.048 1233.19 185.273C1233.19 260.939 132.851 180.191 132.851 290.767C132.851 398.34 1418.02 312.25 1418.02 500.447C1418.02 623.797 -396.649 572.381 -396.649 572.381L-394.969 572.68C-394.969 572.68 1436.35 648.6 1436.35 499.446C1433.53 305.242 144.291 387.337 144.291 290.767C144.291 183.271 1240.57 268.705 1240.57 185.273C1240.57 117.265 342.354 137.568 342.354 106.112Z" fill="url(#paint3_linear_14_397)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter4_dd_14_397)">
|
||||
<path d="M41.3506 583.043C41.3506 585.084 35.0826 588 27.3506 588C19.6186 588 13.3506 584.793 13.3506 583.043C13.3506 581.294 13.3506 580 13.3506 580H41.3506C41.3506 580 41.3506 581.002 41.3506 583.043Z" fill="url(#paint4_linear_14_397)"/>
|
||||
</g>
|
||||
<ellipse cx="27.3506" cy="580.5" rx="14" ry="5.5" fill="#C19D72" />
|
||||
<ellipse cx="27.3506" cy="580.5" rx="14" ry="5.5" fill="url(#paint5_linear_14_397)" fill-opacity="0.4" />
|
||||
<g filter="url(#filter5_i_14_397)">
|
||||
<ellipse cx="27.8506" cy="580.5" rx="12.5" ry="4.5" fill="#C9A479" />
|
||||
<ellipse cx="27.8506" cy="580.5" rx="12.5" ry="4.5" fill="url(#paint6_linear_14_397)" fill-opacity="0.8" style="mix-blend-mode: color-dodge" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: multiply" opacity="0.4" filter="url(#filter6_f_14_397)">
|
||||
<ellipse cx="2" cy="0.5" rx="2" ry="0.5" transform="matrix(1 0 0 -1 17.3506 581)" fill="#E4CAAA" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: multiply" filter="url(#filter7_f_14_397)">
|
||||
<ellipse cx="2" cy="0.5" rx="2" ry="0.5" transform="matrix(1 0 0 -1 25.3506 581)" fill="#E4CAAA" />
|
||||
</g>
|
||||
<g style="mix-blend-mode: multiply" filter="url(#filter8_f_14_397)">
|
||||
<ellipse cx="1" cy="5e-05" rx="1" ry="5e-05" transform="matrix(1 0 0 -1 26.3506 581)" fill="#E4CAAA" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_14_397" x="-338.749" y="67" width="1804.1" height="539" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation="2" result="effect1_foregroundBlur_14_397" />
|
||||
</filter>
|
||||
<filter id="filter1_iin_14_397" x="-349.649" y="70" width="1814" height="531" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="-2" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.758013 0 0 0 0 0.700919 0 0 0 0 0.586731 0 0 0 0.5 0" />
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_14_397" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.895833 0 0 0 0 0.883702 0 0 0 0 0.839844 0 0 0 0.15 0" />
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_14_397" result="effect2_innerShadow_14_397" />
|
||||
<feTurbulence type="fractalNoise" baseFrequency="1 1" stitchTiles="stitch" numOctaves="3" result="noise" seed="8423" />
|
||||
<feColorMatrix in="noise" type="luminanceToAlpha" result="alphaNoise" />
|
||||
<feComponentTransfer in="alphaNoise" result="coloredNoise1">
|
||||
<feFuncA type="discrete" tableValues="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 " />
|
||||
</feComponentTransfer>
|
||||
<feComposite operator="in" in2="effect2_innerShadow_14_397" in="coloredNoise1" result="noise1Clipped" />
|
||||
<feFlood flood-color="rgba(213, 197, 167, 0.1)" result="color1Flood" />
|
||||
<feComposite operator="in" in2="noise1Clipped" in="color1Flood" result="color1" />
|
||||
<feMerge result="effect3_noise_14_397">
|
||||
<feMergeNode in="effect2_innerShadow_14_397" />
|
||||
<feMergeNode in="color1" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="filter2_i_14_397" x="-396.649" y="71" width="1834" height="522" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="1" dy="1" />
|
||||
<feGaussianBlur stdDeviation="0.5" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_14_397" />
|
||||
</filter>
|
||||
<filter id="filter3_i_14_397" x="-396.649" y="71" width="1834" height="522" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="1" dy="2" />
|
||||
<feGaussianBlur stdDeviation="0.5" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.786859 0 0 0 0 0.601696 0 0 0 0 0.467828 0 0 0 0.2 0" />
|
||||
<feBlend mode="multiply" in2="shape" result="effect1_innerShadow_14_397" />
|
||||
</filter>
|
||||
<filter id="filter4_dd_14_397" x="2.67029e-05" y="570.358" width="54.7011" height="34.7011" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="3.70849" />
|
||||
<feGaussianBlur stdDeviation="6.67528" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.472756 0 0 0 0 0.254801 0 0 0 0 0.153797 0 0 0 0.25 0" />
|
||||
<feBlend mode="multiply" in2="BackgroundImageFix" result="effect1_dropShadow_14_397" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.4834" />
|
||||
<feGaussianBlur stdDeviation="0.741698" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.472756 0 0 0 0 0.254801 0 0 0 0 0.153797 0 0 0 0.2 0" />
|
||||
<feBlend mode="multiply" in2="effect1_dropShadow_14_397" result="effect2_dropShadow_14_397" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_14_397" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter5_i_14_397" x="15.3506" y="576" width="25" height="9" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dx="0.528863" dy="0.528863" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.790064 0 0 0 0 0.646514 0 0 0 0 0.47733 0 0 0 1 0" />
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_14_397" />
|
||||
</filter>
|
||||
<filter id="filter6_f_14_397" x="12.1747" y="574.824" width="14.3518" height="11.3518" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation="2.58795" result="effect1_foregroundBlur_14_397" />
|
||||
</filter>
|
||||
<filter id="filter7_f_14_397" x="20.1747" y="574.824" width="14.3518" height="11.3518" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation="2.58795" result="effect1_foregroundBlur_14_397" />
|
||||
</filter>
|
||||
<filter id="filter8_f_14_397" x="22.6227" y="577.272" width="9.45577" height="7.45587" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation="1.86394" result="effect1_foregroundBlur_14_397" />
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_14_397" x1="1577.1" y1="685.047" x2="782.331" y2="11.4077" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D9D9D9" stop-opacity="0" />
|
||||
<stop offset="0.0652711" stop-color="#737373" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_14_397" x1="560.851" y1="603.774" x2="560.851" y2="71" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.647646" stop-color="#6F5D3A" />
|
||||
<stop offset="0.85" stop-color="#D5B26F" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_14_397" x1="641.221" y1="47.9779" x2="641.221" y2="746.594" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EFE7D7" />
|
||||
<stop offset="1" stop-color="#EFD7A6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_14_397" x1="32.3402" y1="333.027" x2="1436.35" y2="333.027" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC176" stop-opacity="1" />
|
||||
<stop offset="0.55" stop-color="#FF9A49" />
|
||||
<stop offset="1" stop-color="#FFC176" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_14_397" x1="14.8206" y1="583.937" x2="40.4756" y2="583.918" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AC8B61" />
|
||||
<stop offset="0.5" stop-color="#EECFA8" />
|
||||
<stop offset="1" stop-color="#AC8B61" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_14_397" x1="27.3506" y1="586" x2="27.3506" y2="575" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_14_397" x1="27.8506" y1="585" x2="27.8506" y2="576" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- MO용 게이지 SVG (767px 이하) - 모바일 라인 경로 유지, 컬러·트로피만 PC와 동일 -->
|
||||
<div class="gauge-svg-mo">
|
||||
<svg
|
||||
id="gauge-svg-mo"
|
||||
width="100%"
|
||||
height="auto"
|
||||
viewBox="0 0 339 413"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<defs>
|
||||
<!-- 진행률 마스크: stroke-dashoffset으로 채워진 구간만 노출. 초기값 전부 숨김(0%) → JS가 pathLength 기준으로 갱신 -->
|
||||
<!-- 경로 방향: 하단(-89,395)→상단(83,28) — PC와 동일하게 시작점이 하단(START). JS에서 (1-percent) 역전 보정 불필요 -->
|
||||
<mask id="progressMask_mo">
|
||||
<path
|
||||
id="maskPath-mo"
|
||||
d="M-89 395C-89 395 327.298 448.142 329.5 339.5C332.449 194 14.168 257.33 19.5 173.732C25 87.5 332.802 145.804 322.5 88C313.5 37.5 83.6078 64 81 30C79.0441 4.5 165.5 0 165.5 0H174C174 0 89 2 83.1078 27.8531"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="36"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="stroke-dasharray: 2000; stroke-dashoffset: 2000"
|
||||
/>
|
||||
</mask>
|
||||
<!-- PC와 동일 컬러: 트랙 그라데이션 EFE7D7 → EFD7A6 -->
|
||||
<linearGradient id="paint_track_mo" x1="151.461" y1="-15.7357" x2="151.461" y2="533.166" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EFE7D7"/>
|
||||
<stop offset="1" stop-color="#EFD7A6"/>
|
||||
</linearGradient>
|
||||
<!-- PC와 동일: 진행률 오렌지 그라데이션 FFC176, FF9A49 -->
|
||||
<linearGradient id="paint_progress_mo" x1="0" y1="0" x2="339" y2="413" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC176" stop-opacity="1" />
|
||||
<stop offset="0.55" stop-color="#FF9A49" />
|
||||
<stop offset="1" stop-color="#FFC176" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
<!-- PC와 동일: 트로피/깃발 그라데이션 AC8B61, EECFA8 -->
|
||||
<linearGradient id="paint4_mo" x1="14.82" y1="383.937" x2="40.48" y2="383.918" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AC8B61" />
|
||||
<stop offset="0.5" stop-color="#EECFA8" />
|
||||
<stop offset="1" stop-color="#AC8B61" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_mo" x1="27.35" y1="386" x2="27.35" y2="375" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_mo" x1="27.85" y1="385" x2="27.85" y2="376" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
<filter id="filter_bg_mo" x="-80" y="0" width="420.725" height="417.473" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.758013 0 0 0 0 0.700919 0 0 0 0 0.586731 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_mo"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.895833 0 0 0 0 0.883702 0 0 0 0 0.839844 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_mo" result="effect2_innerShadow_mo"/>
|
||||
<feTurbulence type="fractalNoise" baseFrequency="1 1" stitchTiles="stitch" numOctaves="3" result="noise" seed="8423" />
|
||||
<feColorMatrix in="noise" type="luminanceToAlpha" result="alphaNoise" />
|
||||
<feComponentTransfer in="alphaNoise" result="coloredNoise1">
|
||||
<feFuncA type="discrete" tableValues="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "/>
|
||||
</feComponentTransfer>
|
||||
<feComposite operator="in" in2="effect2_innerShadow_mo" in="coloredNoise1" result="noise1Clipped" />
|
||||
<feFlood flood-color="rgba(213, 197, 167, 0.1)" result="color1Flood" />
|
||||
<feComposite operator="in" in2="noise1Clipped" in="color1Flood" result="color1" />
|
||||
<feMerge result="effect3_noise_mo">
|
||||
<feMergeNode in="effect2_innerShadow_mo" />
|
||||
<feMergeNode in="color1" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="filter_base_mo" x="-89" y="0" width="429" height="413.219" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1" dy="1"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_base_mo"/>
|
||||
</filter>
|
||||
<!-- 트로피 그림자 (PC와 동일 스타일) -->
|
||||
<filter id="filter_flag_mo" x="-25" y="365" width="80" height="50" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.472756 0 0 0 0 0.254801 0 0 0 0 0.153797 0 0 0 0.25 0" />
|
||||
<feBlend mode="multiply" in2="BackgroundImageFix" result="effect1_dropShadow_mo" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_mo" result="shape" />
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- 1) 배경: 모바일 라인 bg1 경로 + PC 동일 컬러(EFE7D7→EFD7A6) -->
|
||||
<g filter="url(#filter_bg_mo)">
|
||||
<path d="M84.4992 29C93.4992 0 196.5 1 196.5 1L166.5 0C166.5 0 79.9715 4.2353 78.8967 29.7713C78.8967 68.6117 320.499 39.3452 320.499 91.9475C320.499 144.55 17.5 82.9594 17.5 177.744C21 258 327.499 198.985 327.499 338C327.499 440 -80.0007 397.357 -80.0007 397.357L-77.3959 400.043C-77.3959 400.043 332.015 465.578 340.498 342.5C350.363 199.378 34.4539 241.648 27.4985 177.744C18.9143 98.8752 332.62 151.112 328.5 91.9475C324.5 34.5 75.4992 58 84.4992 29Z" fill="url(#paint_track_mo)"/>
|
||||
</g>
|
||||
<!-- 2) 베이스 트랙: 모바일 라인 1 + PC 동일 베이지 #CCBDA1 -->
|
||||
<g filter="url(#filter_base_mo)">
|
||||
<path d="M83.1078 27.8531C89 2 174 0 174 0H165.5C165.5 0 79.0441 4.5 81 30C83.6078 64 313.5 37.5 322.5 88C332.802 145.804 25 87.5 19.5 173.732C14.168 257.33 332.449 194 329.5 339.5C327.298 448.142 -89 395 -89 395C-89 395 339 460.5 339 336C335.5 198.961 33 245.5 25.5 175.5C24 99.7683 322.5 143.5 326.5 93.5C331.179 35.014 75.6673 60.5 83.1078 27.8531Z" fill="#CCBDA1"/>
|
||||
</g>
|
||||
<!-- 3) 진행률: PC와 동일 오렌지 그라데이션 -->
|
||||
<g mask="url(#progressMask_mo)">
|
||||
<path d="M83.1078 27.8531C89 2 174 0 174 0H165.5C165.5 0 79.0441 4.5 81 30C83.6078 64 313.5 37.5 322.5 88C332.802 145.804 25 87.5 19.5 173.732C14.168 257.33 332.449 194 329.5 339.5C327.298 448.142 -89 395 -89 395C-89 395 339 460.5 339 336C335.5 198.961 33 245.5 25.5 175.5C24 99.7683 322.5 143.5 326.5 93.5C331.179 35.014 75.6673 60.5 83.1078 27.8531Z" fill="url(#paint_progress_mo)"/>
|
||||
</g>
|
||||
<!-- 4) 트로피/깃발: PC와 동일 컬러·스타일, 모바일 경로 시작점(-89,395) 근처 배치 -->
|
||||
<g filter="url(#filter_flag_mo)">
|
||||
<path d="M31.35 393.04C31.35 395.08 25.08 398 17.35 398C9.62 398 3.35 394.79 3.35 393.04C3.35 391.29 3.35 390 3.35 390H31.35C31.35 390 31.35 391 31.35 393.04Z" fill="url(#paint4_mo)"/>
|
||||
</g>
|
||||
<ellipse cx="17.35" cy="390.5" rx="9" ry="3.5" fill="#C19D72" />
|
||||
<ellipse cx="17.35" cy="390.5" rx="9" ry="3.5" fill="url(#paint5_mo)" fill-opacity="0.4" />
|
||||
<ellipse cx="17.85" cy="390.5" rx="8" ry="3" fill="#C9A479" />
|
||||
<ellipse cx="17.85" cy="390.5" rx="8" ry="3" fill="url(#paint6_mo)" fill-opacity="0.8" style="mix-blend-mode: color-dodge" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* PC/모바일 게이지 전환 (768px 기준) */
|
||||
.gauge-svg-pc { display: block; }
|
||||
.gauge-svg-mo { display: none; }
|
||||
@media (max-width: 767px) {
|
||||
.gauge-svg-pc { display: none; }
|
||||
.gauge-svg-mo { display: block; }
|
||||
}
|
||||
</style>
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bbs/auth.php';
|
||||
require_once __DIR__ . '/../bbs/db_conn.php';
|
||||
|
||||
edu_start_session();
|
||||
|
||||
if (edu_is_logged_in()) {
|
||||
header('Location: /skin/index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$redirect = trim((string) ($_GET['redirect'] ?? '/skin/index.php'));
|
||||
if ($redirect === '' || strpos($redirect, '/') !== 0) {
|
||||
$redirect = '/skin/index.php';
|
||||
}
|
||||
|
||||
$errorMessage = trim((string) ($_GET['error'] ?? ''));
|
||||
$memberIdInput = trim((string) ($_GET['member_id'] ?? ''));
|
||||
$sysCompCode = trim((string) ($_GET['sys_comp_code'] ?? ''));
|
||||
|
||||
// 법인 목록 조회 (CO100)
|
||||
$corp_list = [];
|
||||
try {
|
||||
$pdo = db_conn();
|
||||
$stmt_corp = $pdo->query("CALL proc_get_code2_list('CO100')");
|
||||
$corp_list = $stmt_corp->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($stmt_corp->nextRowset()) {}
|
||||
unset($stmt_corp, $pdo);
|
||||
} catch (Exception $e) {
|
||||
$corp_list = [];
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>배움터 로그인</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-1: #082a26;
|
||||
--bg-2: #0b4d45;
|
||||
--panel: #f2efe5;
|
||||
--text: #1d2522;
|
||||
--accent: #0e6b5f;
|
||||
--accent-dark: #0a4f46;
|
||||
--error: #b93737;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: 'Noto Sans KR', sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(1200px 600px at 80% -20%, rgba(255, 255, 255, 0.16), transparent 60%),
|
||||
linear-gradient(140deg, var(--bg-1), var(--bg-2));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
width: min(420px, 100%);
|
||||
background: var(--panel);
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.26);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin: 0 0 6px;
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
color: #0a4f46;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
color: #4c5755;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #c9d0cd;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%234c5755' d='M6 8L0 0h12z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field input {
|
||||
background-image: none;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field select:focus {
|
||||
border-color: #1a8f80;
|
||||
box-shadow: 0 0 0 3px rgba(26, 143, 128, 0.18);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
margin-top: 8px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #17a08f, var(--accent));
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: linear-gradient(180deg, #159182, var(--accent-dark));
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0 0 14px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fbe8e8;
|
||||
color: var(--error);
|
||||
font-size: 13px;
|
||||
border: 1px solid #f2caca;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<h1 class="brand">배움터</h1>
|
||||
<p class="sub">사내 계정으로 로그인하세요.</p>
|
||||
|
||||
<?php if ($errorMessage !== ''): ?>
|
||||
<p class="error"><?= htmlspecialchars($errorMessage, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/bbs/login.php" autocomplete="off">
|
||||
<input type="hidden" name="redirect"
|
||||
value="<?= htmlspecialchars($redirect, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
<div class="field">
|
||||
<select name="sys_comp_code" required>
|
||||
<option value="">회사코드 선택</option>
|
||||
<?php foreach ($corp_list as $corp): ?>
|
||||
<option value="<?= htmlspecialchars($corp['code'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
<?= $sysCompCode === $corp['code'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($corp['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="text" name="member_id" placeholder="ID"
|
||||
value="<?= htmlspecialchars($memberIdInput, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="password" name="intra_pw" placeholder="PW" required />
|
||||
</div>
|
||||
<button class="btn-login" type="submit">로그인</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/_include/_auth.php';
|
||||
|
||||
edu_start_session();
|
||||
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], (bool)$params['secure'], (bool)$params['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
|
||||
header('Location: /skin/login.php');
|
||||
exit;
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<!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"></div>
|
||||
|
||||
<!-- 메인 페이지 스크립트 (의존성 순서 유지하며 defer로 비동기 로드) -->
|
||||
<script src="/js/main/Videomodalmanager.js" defer></script>
|
||||
<script>
|
||||
// defer 스크립트가 로드된 후 실행되도록 DOMContentLoaded 사용
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const videos = [
|
||||
{
|
||||
id: 1,
|
||||
url: "Qjig6SHwZyE",
|
||||
category: "리더십",
|
||||
subcate: "코칭",
|
||||
bookmark: true,
|
||||
title: "목표관리, 성과관리의 차이? 더 중요한 것은?",
|
||||
picker: "홍길동",
|
||||
type: "main",
|
||||
keywords: ["소통", "건강"],
|
||||
gauge: 8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: "a2l1uZfsRi0",
|
||||
category: "인사이트",
|
||||
subcate: "경제와사회",
|
||||
bookmark: false,
|
||||
title: "회계를 조금이라도 이해하면 인생이 달라지는 이유",
|
||||
picker: "",
|
||||
type: "comment",
|
||||
keywords: ["소통", "코칭"],
|
||||
gauge: 80,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: "8MugD6Cwhl8",
|
||||
category: "온보딩",
|
||||
subcate: "축적의 시간",
|
||||
bookmark: false,
|
||||
title: "축적의 시간 - 착각의 시간",
|
||||
picker: "",
|
||||
type: "onboarding",
|
||||
keywords: ["마인드셋", "자기개발"],
|
||||
gauge: 35,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: "Py7nutVN53s",
|
||||
category: "법정교육",
|
||||
subcate: "",
|
||||
title: "개인정보보호",
|
||||
type: "learning",
|
||||
keywords: ["안전", "중간관리자"],
|
||||
},
|
||||
];
|
||||
|
||||
// 비디오 모달 매니저 초기화
|
||||
const modalManager = new VideoModalManager({
|
||||
videos: videos,
|
||||
});
|
||||
|
||||
// 모달 매니저 초기화
|
||||
modalManager.init();
|
||||
|
||||
// ========================================
|
||||
// URL 파라미터로 모달 자동 오픈
|
||||
// ========================================
|
||||
|
||||
// VideoModalManager는 각 비디오의 type에 따라 자동으로 다른 모달 HTML을 로드합니다:
|
||||
// ?video=1 (type: "main") → ./_modal/video.html
|
||||
// ?video=2 (type: "comment") → ./_modal/video-comment.html
|
||||
// ?video=3 (type: "onboarding") → ./_modal/video-onboarding.html
|
||||
// ?video=4 (type: "learning") → ./_modal/video-learning.html
|
||||
// ?keyword=true → ./_modal/keyword.html
|
||||
|
||||
// 키워드 모달 Ajax 로드 함수
|
||||
async function loadKeywordModal() {
|
||||
try {
|
||||
const response = await fetch("/skin/_modal/keyword.php");
|
||||
if (!response.ok) {
|
||||
throw new Error("키워드 모달 로드 실패");
|
||||
}
|
||||
|
||||
const modalHTML = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(modalHTML, "text/html");
|
||||
const modalElement = doc.querySelector(".modal.keyword");
|
||||
|
||||
if (!modalElement) {
|
||||
throw new Error("키워드 모달 요소를 찾을 수 없습니다");
|
||||
}
|
||||
|
||||
// DOM에 추가
|
||||
document.body.appendChild(modalElement);
|
||||
|
||||
// 모달 표시
|
||||
setTimeout(() => {
|
||||
modalElement.style.display = "block";
|
||||
}, 50);
|
||||
|
||||
// 닫기 이벤트 설정
|
||||
setupKeywordModalCloseEvents(modalElement);
|
||||
} catch (error) {
|
||||
console.error("키워드 모달 로드 오류:", error);
|
||||
alert("키워드 모달을 로드하는 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
// 키워드 모달 닫기 이벤트 (ModalUtils 활용)
|
||||
function setupKeywordModalCloseEvents(modal) {
|
||||
// ModalUtils를 사용하여 공통 닫기 이벤트 설정
|
||||
ModalUtils.setupCloseEvents(modal, {
|
||||
closeSelector: ".ico-check", // 키워드 모달은 .ico-check를 닫기 버튼으로 사용
|
||||
onClose: () => {
|
||||
ModalUtils.remove(modal, { duration: 300 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function checkURLParamsAndOpenModal() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// 비디오 모달 오픈: ?video=1
|
||||
const videoId = urlParams.get("video");
|
||||
if (videoId) {
|
||||
const id = parseInt(videoId);
|
||||
if (!isNaN(id)) {
|
||||
const video = videos.find((v) => v.id === id);
|
||||
if (video) {
|
||||
console.log(
|
||||
`비디오 모달 자동 오픈: ID ${id}, Type: ${video.type}`
|
||||
);
|
||||
setTimeout(() => {
|
||||
modalManager.loadVideoModal(id);
|
||||
}, 500);
|
||||
} else {
|
||||
console.error(`비디오 ID ${id}를 찾을 수 없습니다.`);
|
||||
}
|
||||
return; // 비디오 모달 열면 종료
|
||||
}
|
||||
}
|
||||
|
||||
// 키워드 모달 오픈: ?keyword=true
|
||||
const openKeyword = urlParams.get("keyword");
|
||||
if (openKeyword === "true") {
|
||||
console.log("키워드 모달 자동 오픈 (Ajax 로드)");
|
||||
setTimeout(() => {
|
||||
loadKeywordModal();
|
||||
}, 500);
|
||||
return; // 키워드 모달 열면 종료
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 완료 후 URL 파라미터 체크
|
||||
window.addEventListener("DOMContentLoaded", checkURLParamsAndOpenModal);
|
||||
}); // DOMContentLoaded 종료
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,860 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/auth.php';
|
||||
edu_require_login();
|
||||
|
||||
$mode = isset($_GET['mode']) ? trim((string) $_GET['mode']) : 'initial';
|
||||
$completedGoalId = isset($_GET['completed_goal']) ? (int) $_GET['completed_goal'] : 1;
|
||||
$debugRecsRaw = isset($_GET['debug_recs']) ? strtolower(trim((string)$_GET['debug_recs'])) : '';
|
||||
$debugRecs = in_array($debugRecsRaw, ['1', 'true', 'yes', 'on', 'y', 'debug'], true)
|
||||
|| (isset($_SERVER['QUERY_STRING']) && strpos((string)$_SERVER['QUERY_STRING'], 'debug_recs') !== false);
|
||||
$debugRecsPayload = [];
|
||||
if ($debugRecs && !headers_sent()) {
|
||||
header('X-Debug-Recs: on');
|
||||
}
|
||||
// ========== DB 연동 초기화 ==========
|
||||
|
||||
require_once __DIR__ . '/../bbs/db_conn.php';
|
||||
|
||||
// 세션 정보로부터 사용자 ID 및 정보 추출
|
||||
$memberId = $_SESSION['member_id'] ?? null;
|
||||
$userName = $_SESSION['member_name'] ?? $_SESSION['user_name'] ?? '사용자';
|
||||
$userPosition = $_SESSION['member_rank'] ?? $_SESSION['user_position'] ?? '';
|
||||
$userCompany = $_SESSION['sys_comp_code'] ?? '';
|
||||
|
||||
// ========== 년도/분기/남은일수 계산 ==========
|
||||
$today = new DateTime('now', new DateTimeZone('Asia/Seoul'));
|
||||
$currentYear = (int) $today->format('Y');
|
||||
$currentMonth = (int) $today->format('m');
|
||||
$currentQuarterNum = (int) ceil($currentMonth / 3);
|
||||
$currentQuarterCode = sprintf('CA200Q%02d', $currentQuarterNum);
|
||||
$modalQuarterLabel = substr((string)$currentYear, 2) . '년 ' . $currentQuarterNum . '분기)';
|
||||
|
||||
// DB에서 추가 정보 보충 (member_id + sys_comp_code 복합키 기준)
|
||||
if (isset($memberId) && $memberId !== null && $memberId !== '' && $userCompany !== '' && ($userPosition === '' || $userName === '' || $userName === '사용자')) {
|
||||
try {
|
||||
$stmt = db_conn()->prepare(
|
||||
'SELECT name, rank_name, sys_comp_code FROM edu_users WHERE member_id = ? AND sys_comp_code = ? LIMIT 1'
|
||||
);
|
||||
$stmt->execute([(string)$memberId, (string)$userCompany]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($userName === '' || $userName === '사용자') {
|
||||
$userName = $row['name'] ?? '사용자';
|
||||
}
|
||||
if ($userPosition === '') {
|
||||
$userPosition = $row['rank_name'] ?? '';
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[myclass.php] DB fetch error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 목표 설정 여부 확인 → 이미 설정한 사용자는 myclass_list.php로 바로 이동 ==========
|
||||
// mode=initial(기본 진입)일 때만 체크. completed/additional 모드는 목표 완료 플로우이므로 통과.
|
||||
if ($mode === 'initial' && isset($memberId) && !$debugRecs) {
|
||||
try {
|
||||
$sysCompCodeCheck = (string)($userCompany ?? '');
|
||||
$hasGoal = false;
|
||||
|
||||
if ($memberId !== null && $memberId !== '' && $sysCompCodeCheck !== '') {
|
||||
$stmtGoalCheck = db_conn()->prepare(
|
||||
"SELECT COUNT(*) FROM edu_user_learning_goals
|
||||
WHERE member_id = ?
|
||||
AND sys_comp_code = ?
|
||||
AND quarter = ?
|
||||
AND is_active = '1'
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmtGoalCheck->execute([(string)$memberId, $sysCompCodeCheck, $currentQuarterCode]);
|
||||
$hasGoal = (int)$stmtGoalCheck->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
if ($hasGoal) {
|
||||
header('Location: /skin/myclass_list.php');
|
||||
exit;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[myclass.php] goal check error: ' . $e->getMessage());
|
||||
// 오류 시 그냥 myclass.php 렌더링 계속
|
||||
}
|
||||
}
|
||||
|
||||
// 분기 마지막 날짜
|
||||
$quarterEndDates = [
|
||||
1 => '03-31', 2 => '06-30', 3 => '09-30', 4 => '12-31'
|
||||
];
|
||||
$todayDateOnly = (clone $today)->setTime(0, 0, 0);
|
||||
$targetDateStr = $currentYear . '-' . $quarterEndDates[$currentQuarterNum];
|
||||
$targetDate = new DateTime($targetDateStr, new DateTimeZone('Asia/Seoul'));
|
||||
$targetDate->setTime(0, 0, 0);
|
||||
$daysInterval = $todayDateOnly->diff($targetDate);
|
||||
$daysRemaining = max(0, (int) $daysInterval->format('%a'));
|
||||
$daysDisplay = 'D-' . $daysRemaining;
|
||||
|
||||
// 분기 마지막 날짜 (한글)
|
||||
$endDateParts = explode('-', $quarterEndDates[$currentQuarterNum]);
|
||||
$endMonth = (int) $endDateParts[0];
|
||||
$endDay = (int) $endDateParts[1];
|
||||
$targetDateKr = $endMonth . '월 ' . $endDay . '일';
|
||||
|
||||
$completedGoalId = max(1, min(6, $completedGoalId));
|
||||
|
||||
$defaultGoals = [
|
||||
['id' => 1, 'title' => '자기이해와 강점 찾기', 'desc' => '나의 성향, 가치관, 강점 등을 탐색하고 이해하며 자기인식을 높이는 과정', 'icon' => '01', 'gif' => 'ico_study_01.png', 'json' => 'ico_study_01.json'],
|
||||
['id' => 2, 'title' => '효과적인\n의사소통 배우기', 'desc' => '상황에 맞는 말하기와 경청을 통해 타인과 원활하게 소통하는 방법을 익히는 과정', 'icon' => '02', 'gif' => 'ico_study_02.png', 'json' => 'ico_study_02.json'],
|
||||
['id' => 3, 'title' => '감정 조절과 자기관리', 'desc' => '다양한 감정을 인식하고 조절하여 안정적인 삶을 유지하는 자기관리 훈련 과정', 'icon' => '03', 'gif' => 'ico_study_03.png', 'json' => 'ico_study_03.json'],
|
||||
['id' => 4, 'title' => '비판적 사고와 문제 해결', 'desc' => '다양한 관점에서 생각하고 문제 상황을 논리적으로 해결하는 능력을 기르는 과정', 'icon' => '04', 'gif' => 'ico_study_04.png', 'json' => 'ico_study_04.json'],
|
||||
['id' => 5, 'title' => '협업과 팀워크 기르기', 'desc' => '타인과 협력하며 공동의 목표를 위해 함께 노력하는 태도와 기술을 기르는 과정', 'icon' => '05', 'gif' => 'ico_study_05.png', 'json' => 'ico_study_05.json'],
|
||||
['id' => 6, 'title' => '커리어 탐색과 역량 개발', 'desc' => '직무와 산업의 흐름을 이해하고, 자신의 커리어 방향성과 필요한 역량을 점검·계획하는 과정', 'icon' => '06', 'gif' => 'ico_study_06.png', 'json' => 'ico_study_06.json'],
|
||||
];
|
||||
|
||||
$defaultModalBooks = [
|
||||
['id' => 1, 'youtube' => 'KE_MeQZgnPM', 'title' => '조직도가 리셋된다!', 'sub' => '위계 중심 조직은\n더 이상 통하지 않습니다.', 'main' => '더 유연하게\n일할 방법', 'tag' => 'IT 테크', 'img' => 'img_book_01'],
|
||||
['id' => 2, 'youtube' => 'a2l1uZfsRi0', 'title' => '혈당 스파이크', 'sub' => '식후 졸림은 의지 문제가\n아니라 혈당의 문제!', 'main' => '혈당을 안정시켜줄\n실생활 관리법', 'tag' => '웰니스', 'img' => 'img_book_02'],
|
||||
['id' => 3, 'youtube' => 'IeF8r0ycgVg', 'title' => '제대로 쉬는 방법', 'sub' => '아무리 자도\n피곤한가요?', 'main' => '진짜 회복되는 쉼이\n무엇인지 알게됩니다.', 'tag' => '마인드셋', 'img' => 'img_book_03'],
|
||||
['id' => 4, 'youtube' => 'KMZXMI0QPoA', 'title' => "'AI 도파민'의 바다에 빠진 이유", 'sub' => 'AI는 선택이 아닌\n생존의 도구', 'main' => 'AI는 도구가 아닌\n업무 파트너', 'tag' => 'IT 테크', 'img' => 'img_book_04'],
|
||||
['id' => 5, 'youtube' => 'CRKwszz6l2M', 'title' => '살찌고 망가진몸 되살리는 방법', 'sub' => '몸을 망쳤다면?\n다이어트 아닌 리셋이 답!', 'main' => '건강한 몸 되찾기', 'tag' => '웰니스', 'img' => 'img_book_05'],
|
||||
['id' => 6, 'youtube' => 'Gf5WoZ3BmgI', 'title' => '자기 관점의 힘', 'sub' => '당신의 삶을 바꾸는\n가장 강력한 무기?', 'main' => '자신만의 관점으로\n경쟁력을 키우는 방법', 'tag' => '리더십', 'img' => 'img_book_06'],
|
||||
];
|
||||
|
||||
$goals = $defaultGoals;
|
||||
$goalCodeById = [];
|
||||
|
||||
$defaultRecommendedItems = [
|
||||
['title' => '업무에 익숙해졌지만 성장 정체를 느끼는 구성원', 'description' => "창의적 사고를 '아이디어가 아니라 업무를 바라보는 방식으로 재정렬 할 수 있어요!"],
|
||||
['title' => '문제 해결을 요구받기 시작한 주니어, 중급 구성원', 'description' => "창의적 사고를 '아이디어가 아니라 업무를 바라보는 방식으로 재정렬 할 수 있어요!"],
|
||||
['title' => '팀 또는 프로젝트 단위로 사고의 확장이 필요한 구성원', 'description' => "창의적 사고를 '아이디어가 아니라 업무를 바라보는 방식으로 재정렬 할 수 있어요!"],
|
||||
];
|
||||
|
||||
$goalRecommendations = [];
|
||||
for ($i = 1; $i <= 6; $i++) {
|
||||
$goalRecommendations[$i] = $defaultRecommendedItems;
|
||||
}
|
||||
|
||||
$goalBooksById = [];
|
||||
for ($i = 1; $i <= 6; $i++) {
|
||||
$goalBooksById[$i] = [];
|
||||
}
|
||||
|
||||
function myclass_extract_video_id($value) {
|
||||
$value = trim((string)$value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('~(?:v=|\.be/)([A-Za-z0-9_-]{11})~', $value, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/^[A-Za-z0-9_-]{11}$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// 목표 카드 6개: edu_learning_goals.title / edu_learning_goals.remarks 연동
|
||||
try {
|
||||
$stmtGoals = db_conn()->prepare(
|
||||
"SELECT goal_code,
|
||||
title AS goal_title,
|
||||
remarks AS goal_remarks
|
||||
FROM edu_learning_goals
|
||||
WHERE is_active = '1'
|
||||
AND base_year = ?
|
||||
AND quarter = ?
|
||||
ORDER BY goal_code ASC"
|
||||
);
|
||||
$stmtGoals->execute([(string)$currentYear, $currentQuarterCode]);
|
||||
$goalRows = $stmtGoals->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 현재 연도/분기 데이터가 부족하면 동일 분기의 최신 연도로 fallback
|
||||
if (count($goalRows) < 6) {
|
||||
$stmtGoals = db_conn()->query(
|
||||
"SELECT goal_code,
|
||||
title AS goal_title,
|
||||
remarks AS goal_remarks
|
||||
FROM edu_learning_goals
|
||||
WHERE is_active = '1'
|
||||
AND quarter = " . db_conn()->quote($currentQuarterCode) . "
|
||||
ORDER BY base_year DESC, goal_code ASC
|
||||
LIMIT 6"
|
||||
);
|
||||
$goalRows = $stmtGoals->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$goalSlot = 1;
|
||||
foreach ($goalRows as $goalRow) {
|
||||
if ($goalSlot > 6) {
|
||||
break;
|
||||
}
|
||||
|
||||
$goalCode = (string)($goalRow['goal_code'] ?? '');
|
||||
$title = trim((string)($goalRow['goal_title'] ?? ''));
|
||||
$desc = trim((string)($goalRow['goal_remarks'] ?? ''));
|
||||
|
||||
if ($goalCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$goalId = $goalSlot;
|
||||
|
||||
$goalCodeById[$goalId] = $goalCode;
|
||||
|
||||
if ($title !== '') {
|
||||
$goals[$goalId - 1]['title'] = $title;
|
||||
}
|
||||
if ($desc !== '') {
|
||||
$goals[$goalId - 1]['desc'] = $desc;
|
||||
}
|
||||
|
||||
$goalSlot++;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[myclass.php] edu_learning_goals fetch error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 목표별 추천 3개: goal_code + seq 기준으로 연동 (rec-text는 title2 우선 사용)
|
||||
try {
|
||||
$goalCodes = array_values(array_unique(array_filter($goalCodeById)));
|
||||
if (count($goalCodes) > 0) {
|
||||
$normalizeGoalCode = static function ($value) {
|
||||
return strtoupper(trim((string)$value));
|
||||
};
|
||||
|
||||
$goalIdByCode = [];
|
||||
foreach ($goalCodeById as $goalId => $goalCodeRaw) {
|
||||
$normalizedCode = $normalizeGoalCode($goalCodeRaw);
|
||||
if ($normalizedCode === '') {
|
||||
continue;
|
||||
}
|
||||
$goalIdByCode[$normalizedCode] = (int)$goalId;
|
||||
}
|
||||
|
||||
$normalizedGoalCodes = [];
|
||||
foreach ($goalCodes as $goalCodeRaw) {
|
||||
$normalizedCode = $normalizeGoalCode($goalCodeRaw);
|
||||
if ($normalizedCode !== '') {
|
||||
$normalizedGoalCodes[] = $normalizedCode;
|
||||
}
|
||||
}
|
||||
$normalizedGoalCodes = array_values(array_unique($normalizedGoalCodes));
|
||||
if (count($normalizedGoalCodes) === 0) {
|
||||
throw new RuntimeException('goal codes are empty after normalization');
|
||||
}
|
||||
|
||||
$goalSuffixes = [];
|
||||
foreach (array_keys($goalIdByCode) as $normalizedCode) {
|
||||
if (preg_match('/-(\d{3})$/', $normalizedCode, $m)) {
|
||||
$goalSuffixes[] = '-' . $m[1];
|
||||
}
|
||||
}
|
||||
$goalSuffixes = array_values(array_unique($goalSuffixes));
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($normalizedGoalCodes), '?'));
|
||||
$suffixPlaceholders = count($goalSuffixes) > 0
|
||||
? implode(',', array_fill(0, count($goalSuffixes), '?'))
|
||||
: '';
|
||||
|
||||
$pdo = db_conn();
|
||||
|
||||
$hasColumn = static function (PDO $conn, $tableName, $columnName) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = ?
|
||||
AND column_name = ?"
|
||||
);
|
||||
$stmt->execute([(string)$tableName, (string)$columnName]);
|
||||
return ((int)$stmt->fetchColumn()) > 0;
|
||||
};
|
||||
|
||||
$hasTable = static function (PDO $conn, $tableName) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = ?"
|
||||
);
|
||||
$stmt->execute([(string)$tableName]);
|
||||
return ((int)$stmt->fetchColumn()) > 0;
|
||||
};
|
||||
|
||||
// 운영 환경별 오탈자 대응: edu_recommended_goals / edu_recommended_gaols
|
||||
// 둘 다 존재할 수 있으므로, 실제 현재 목표코드와 매칭되는 row 수가 많은 테이블을 선택
|
||||
$availableRecTables = [];
|
||||
foreach (['edu_recommended_goals', 'edu_recommended_gaols'] as $candTable) {
|
||||
if ($hasTable($pdo, $candTable)) {
|
||||
$availableRecTables[] = $candTable;
|
||||
}
|
||||
}
|
||||
if (count($availableRecTables) === 0) {
|
||||
throw new RuntimeException('recommended goals table not found');
|
||||
}
|
||||
|
||||
$tableScores = [];
|
||||
$recTable = $availableRecTables[0];
|
||||
$bestScore = -1;
|
||||
|
||||
foreach ($availableRecTables as $candTable) {
|
||||
$candHasQuarter = $hasColumn($pdo, $candTable, 'quarter');
|
||||
$candHasBaseYear = $hasColumn($pdo, $candTable, 'base_year');
|
||||
$candHasIsActive = $hasColumn($pdo, $candTable, 'is_active');
|
||||
|
||||
$candCodeCondition = "UPPER(TRIM(goal_code)) IN ($placeholders)";
|
||||
if ($suffixPlaceholders !== '') {
|
||||
$candCodeCondition .= " OR RIGHT(UPPER(TRIM(goal_code)), 4) IN ($suffixPlaceholders)";
|
||||
}
|
||||
|
||||
$candWhere = [];
|
||||
if ($candHasIsActive) {
|
||||
$candWhere[] = "is_active = '1'";
|
||||
}
|
||||
$candWhere[] = "({$candCodeCondition})";
|
||||
if ($candHasQuarter) {
|
||||
$candWhere[] = 'quarter = ?';
|
||||
}
|
||||
if ($candHasBaseYear) {
|
||||
$candWhere[] = 'base_year = ?';
|
||||
}
|
||||
|
||||
$candSql = "SELECT COUNT(*) FROM {$candTable} WHERE " . implode(' AND ', $candWhere);
|
||||
$candParams = $normalizedGoalCodes;
|
||||
if (count($goalSuffixes) > 0) {
|
||||
$candParams = array_merge($candParams, $goalSuffixes);
|
||||
}
|
||||
if ($candHasQuarter) {
|
||||
$candParams[] = $currentQuarterCode;
|
||||
}
|
||||
if ($candHasBaseYear) {
|
||||
$candParams[] = (string)$currentYear;
|
||||
}
|
||||
|
||||
$stmtScore = $pdo->prepare($candSql);
|
||||
$stmtScore->execute($candParams);
|
||||
$score = (int)$stmtScore->fetchColumn();
|
||||
$tableScores[$candTable] = $score;
|
||||
|
||||
if ($score > $bestScore) {
|
||||
$bestScore = $score;
|
||||
$recTable = $candTable;
|
||||
}
|
||||
}
|
||||
|
||||
$hasTitle2Col = $hasColumn($pdo, $recTable, 'title2');
|
||||
$hasQuarterCol = $hasColumn($pdo, $recTable, 'quarter');
|
||||
$hasBaseYearCol = $hasColumn($pdo, $recTable, 'base_year');
|
||||
$hasIsActiveCol = $hasColumn($pdo, $recTable, 'is_active');
|
||||
$hasSeqCol = $hasColumn($pdo, $recTable, 'seq');
|
||||
$hasSortOrderCol = $hasColumn($pdo, $recTable, 'sort_order');
|
||||
$hasDescCol = $hasColumn($pdo, $recTable, 'description');
|
||||
|
||||
$title2Expr = $hasTitle2Col ? 'rg.title2' : "''";
|
||||
$recDescExpr = $hasDescCol ? 'rg.description' : "''";
|
||||
$seqExpr = $hasSeqCol ? 'rg.seq' : ($hasSortOrderCol ? 'rg.sort_order' : '0');
|
||||
$joinSeqCondition = '';
|
||||
if ($hasSeqCol) {
|
||||
$joinSeqCondition = ' AND c.sort_order = rg.seq';
|
||||
} elseif ($hasSortOrderCol) {
|
||||
$joinSeqCondition = ' AND c.sort_order = rg.sort_order';
|
||||
}
|
||||
|
||||
$codeCondition = "UPPER(TRIM(rg.goal_code)) IN ($placeholders)";
|
||||
if ($suffixPlaceholders !== '') {
|
||||
$codeCondition .= " OR RIGHT(UPPER(TRIM(rg.goal_code)), 4) IN ($suffixPlaceholders)";
|
||||
}
|
||||
|
||||
$whereConditions = [];
|
||||
if ($hasIsActiveCol) {
|
||||
$whereConditions[] = "rg.is_active = '1'";
|
||||
}
|
||||
$whereConditions[] = "({$codeCondition})";
|
||||
if ($hasQuarterCol) {
|
||||
$whereConditions[] = 'rg.quarter = ?';
|
||||
}
|
||||
if ($hasBaseYearCol) {
|
||||
$whereConditions[] = 'rg.base_year = ?';
|
||||
}
|
||||
|
||||
$whereSql = implode("\n AND ", $whereConditions);
|
||||
|
||||
$stmtRec = $pdo->prepare(
|
||||
"SELECT rg.goal_code,
|
||||
{$seqExpr} AS seq,
|
||||
rg.title AS rec_title,
|
||||
{$recDescExpr} AS rec_description,
|
||||
{$title2Expr} AS rec_title2,
|
||||
c.title AS content_title,
|
||||
c.description AS content_description
|
||||
FROM {$recTable} rg
|
||||
LEFT JOIN edu_contents c
|
||||
ON c.goal_code = rg.goal_code
|
||||
{$joinSeqCondition}
|
||||
AND c.is_active = '1'
|
||||
WHERE {$whereSql}
|
||||
ORDER BY rg.goal_code ASC, seq ASC"
|
||||
);
|
||||
|
||||
$stmtRecParams = $normalizedGoalCodes;
|
||||
if (count($goalSuffixes) > 0) {
|
||||
$stmtRecParams = array_merge($stmtRecParams, $goalSuffixes);
|
||||
}
|
||||
if ($hasQuarterCol) {
|
||||
$stmtRecParams[] = $currentQuarterCode;
|
||||
}
|
||||
if ($hasBaseYearCol) {
|
||||
$stmtRecParams[] = (string)$currentYear;
|
||||
}
|
||||
|
||||
$stmtRec->execute($stmtRecParams);
|
||||
$recRows = $stmtRec->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($debugRecs) {
|
||||
$debugRecsPayload['table'] = $recTable;
|
||||
$debugRecsPayload['tableScores'] = $tableScores;
|
||||
$debugRecsPayload['schema'] = [
|
||||
'hasTitle2Col' => $hasTitle2Col,
|
||||
'hasQuarterCol' => $hasQuarterCol,
|
||||
'hasBaseYearCol' => $hasBaseYearCol,
|
||||
'hasIsActiveCol' => $hasIsActiveCol,
|
||||
'hasSeqCol' => $hasSeqCol,
|
||||
'hasSortOrderCol' => $hasSortOrderCol,
|
||||
'hasDescCol' => $hasDescCol,
|
||||
];
|
||||
$debugRecsPayload['normalizedGoalCodes'] = $normalizedGoalCodes;
|
||||
$debugRecsPayload['stmtRecParams'] = $stmtRecParams;
|
||||
$debugRecsPayload['recRows'] = $recRows;
|
||||
error_log('[myclass.php][debug_recs] table=' . $recTable . ' normalizedGoalCodes=' . json_encode($normalizedGoalCodes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
error_log('[myclass.php][debug_recs] recRows=' . json_encode($recRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
$fallbackSeqByGoalId = [];
|
||||
|
||||
foreach ($recRows as $recRow) {
|
||||
$goalCode = $normalizeGoalCode($recRow['goal_code'] ?? '');
|
||||
$seq = (int)($recRow['seq'] ?? 0);
|
||||
$recTitle = trim((string)($recRow['rec_title'] ?? ''));
|
||||
$recTitle2 = trim((string)($recRow['rec_title2'] ?? ''));
|
||||
$recDesc = trim((string)($recRow['rec_description'] ?? ''));
|
||||
$contentTitle = trim((string)($recRow['content_title'] ?? ''));
|
||||
$goalId = 0;
|
||||
|
||||
if ($goalCode !== '' && isset($goalIdByCode[$goalCode])) {
|
||||
$goalId = (int)$goalIdByCode[$goalCode];
|
||||
} elseif (preg_match('/-(\d{3})$/', $goalCode, $m)) {
|
||||
$suffixGoalId = (int)$m[1];
|
||||
if ($suffixGoalId >= 1 && $suffixGoalId <= 6) {
|
||||
$goalId = $suffixGoalId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($goalId < 1 || $goalId > 6) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($seq < 1 || $seq > 3) {
|
||||
$fallbackSeqByGoalId[$goalId] = (int)($fallbackSeqByGoalId[$goalId] ?? 0) + 1;
|
||||
$seq = $fallbackSeqByGoalId[$goalId];
|
||||
if ($seq < 1 || $seq > 3) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($recTitle === '') {
|
||||
$recTitle = $contentTitle;
|
||||
}
|
||||
if ($recTitle2 !== '') {
|
||||
$recDesc = $recTitle2;
|
||||
}
|
||||
|
||||
if ($recTitle !== '') {
|
||||
$goalRecommendations[$goalId][$seq - 1]['title'] = $recTitle;
|
||||
}
|
||||
if ($recDesc !== '') {
|
||||
$goalRecommendations[$goalId][$seq - 1]['description'] = $recDesc;
|
||||
}
|
||||
|
||||
if ($debugRecs) {
|
||||
error_log('[myclass.php][debug_recs] mapped goalCode=' . $goalCode . ' seq=' . $seq . ' title=' . $recTitle . ' title2=' . $recTitle2 . ' desc=' . $recDesc);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if ($debugRecs) {
|
||||
$debugRecsPayload['recsError'] = $e->getMessage();
|
||||
}
|
||||
error_log('[myclass.php] edu_recommended_goals fetch error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 목표별 영상 리스트: edu_contents.title/description1/description2/content_url/thumbnail 연동
|
||||
try {
|
||||
$goalCodes = array_values(array_unique(array_filter($goalCodeById)));
|
||||
if (count($goalCodes) > 0) {
|
||||
$goalIdByCode = array_flip($goalCodeById);
|
||||
$placeholders = implode(',', array_fill(0, count($goalCodes), '?'));
|
||||
|
||||
$stmtContents = db_conn()->prepare(
|
||||
"SELECT content_id,
|
||||
goal_code,
|
||||
sort_order,
|
||||
title,
|
||||
description,
|
||||
description1,
|
||||
description2,
|
||||
content_url,
|
||||
thumbnail_url
|
||||
FROM edu_contents
|
||||
WHERE is_active = '1'
|
||||
AND category_code = 'CA10001'
|
||||
AND category_group = ?
|
||||
AND goal_code IN ($placeholders)
|
||||
ORDER BY goal_code ASC, sort_order ASC, content_id ASC"
|
||||
);
|
||||
$stmtContents->execute(array_merge([$currentQuarterCode], $goalCodes));
|
||||
$contentRows = $stmtContents->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($contentRows as $contentRow) {
|
||||
$goalCode = (string)($contentRow['goal_code'] ?? '');
|
||||
if (!isset($goalIdByCode[$goalCode])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$goalId = (int)$goalIdByCode[$goalCode];
|
||||
if (!isset($goalBooksById[$goalId])) {
|
||||
$goalBooksById[$goalId] = [];
|
||||
}
|
||||
|
||||
$contentUrl = trim((string)($contentRow['content_url'] ?? ''));
|
||||
$thumbnailUrl = trim((string)($contentRow['thumbnail_url'] ?? ''));
|
||||
$sortOrder = (int)($contentRow['sort_order'] ?? 0);
|
||||
if ($sortOrder < 1 || $sortOrder > 6) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$templateBook = $defaultModalBooks[$sortOrder - 1] ?? null;
|
||||
if ($templateBook === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($goalBooksById[$goalId]) === 0) {
|
||||
$goalBooksById[$goalId] = $defaultModalBooks;
|
||||
}
|
||||
|
||||
$description1 = trim((string)($contentRow['description1'] ?? ''));
|
||||
$description2 = trim((string)($contentRow['description2'] ?? ''));
|
||||
$description = trim((string)($contentRow['description'] ?? ''));
|
||||
if ($description1 === '') {
|
||||
$description1 = $description;
|
||||
}
|
||||
|
||||
$goalBooksById[$goalId][$sortOrder - 1] = [
|
||||
'id' => (int)($templateBook['id'] ?? $sortOrder),
|
||||
'order' => $sortOrder,
|
||||
'youtube' => ($tmpVideoId = myclass_extract_video_id($contentUrl)) !== '' ? $tmpVideoId : (string)($templateBook['youtube'] ?? ''),
|
||||
'content_url' => $contentUrl,
|
||||
'title' => trim((string)($contentRow['title'] ?? (string)($templateBook['title'] ?? ''))),
|
||||
'sub' => $description1 !== '' ? $description1 : (string)($templateBook['sub'] ?? ''),
|
||||
'main' => $description2 !== '' ? $description2 : (string)($templateBook['main'] ?? ''),
|
||||
'tag' => (string)($templateBook['tag'] ?? ''),
|
||||
'img' => (string)($templateBook['img'] ?? 'img_book_01'),
|
||||
'thumbnail' => $thumbnailUrl,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($goalBooksById as $goalId => $goalBooks) {
|
||||
if (count($goalBooks) > 0) {
|
||||
ksort($goalBooks);
|
||||
$goalBooksById[$goalId] = array_values($goalBooks);
|
||||
} else {
|
||||
$goalBooksById[$goalId] = $defaultModalBooks;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[myclass.php] edu_contents fetch error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($goalBooksById as $goalId => $goalBooks) {
|
||||
if (count($goalBooks) === 0) {
|
||||
$goalBooksById[$goalId] = $defaultModalBooks;
|
||||
}
|
||||
}
|
||||
|
||||
$modalBooks = $goalBooksById[1] ?? $defaultModalBooks;
|
||||
$modalRecItems = $goalRecommendations[1] ?? $defaultRecommendedItems;
|
||||
|
||||
if ($debugRecs) {
|
||||
$debugRecsPayload['goalCodeById'] = $goalCodeById;
|
||||
$debugRecsPayload['goalRecommendations1'] = $goalRecommendations[1] ?? [];
|
||||
$debugRecsPayload['modalRecItems'] = $modalRecItems;
|
||||
error_log('[myclass.php][debug_recs] goalCodeById=' . json_encode($goalCodeById, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
error_log('[myclass.php][debug_recs] goalRecommendations[1]=' . json_encode($goalRecommendations[1] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
error_log('[myclass.php][debug_recs] modalRecItems=' . json_encode($modalRecItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
// 실제 완료된 목표 목록 조회 (DB 기준)
|
||||
$completedGoalIds = [];
|
||||
if (isset($memberId) && $userCompany !== '') {
|
||||
try {
|
||||
$stmtCompleted = db_conn()->prepare(
|
||||
"SELECT g.goal_code, g.title, g.remarks, u.completed_date, u.goal_code AS user_goal_code
|
||||
FROM edu_user_learning_goals u
|
||||
JOIN edu_learning_goals g ON u.goal_code = g.goal_code AND g.is_active = '1'
|
||||
WHERE u.member_id = ?
|
||||
AND u.sys_comp_code = ?
|
||||
AND u.is_active = '1'
|
||||
AND g.quarter = ?
|
||||
AND u.completed_date IS NOT NULL"
|
||||
);
|
||||
$stmtCompleted->execute([(string)$memberId, (string)$userCompany, $currentQuarterCode]);
|
||||
$rowsCompleted = $stmtCompleted->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rowsCompleted as $row) {
|
||||
// goalCodeById: [1=>goal_code1, 2=>goal_code2, ...]
|
||||
$goalCode = (string)($row['user_goal_code'] ?? $row['goal_code'] ?? '');
|
||||
$goalId = array_search($goalCode, $goalCodeById, true);
|
||||
if ($goalId !== false) {
|
||||
$completedGoalIds[] = (int)$goalId;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[myclass.php] fetch completed goals error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// completed/additional 화면은 DB completed_date(트리거 결과)가 확인될 때만 허용
|
||||
$isTriggeredCompletion = in_array($completedGoalId, $completedGoalIds, true);
|
||||
if (in_array($mode, ['completed', 'additional'], true) && !$isTriggeredCompletion) {
|
||||
$mode = 'initial';
|
||||
}
|
||||
|
||||
$completedGoal = $goals[$completedGoalId - 1];
|
||||
$isCompletionFlow = in_array($mode, ['completed', 'additional'], true);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . '/_include/_head.php'); ?>
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap myclass">
|
||||
<?php include(__DIR__ . '/_include/_header.php'); ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="myclass-wrap">
|
||||
<div class="myclass-top-bar">
|
||||
<div class="top-bar-left">
|
||||
<!-- <div class="user-rank-area">
|
||||
<div class="user-rank-area-inner">
|
||||
<div class="user-profile">
|
||||
<img src="/img/insight/profile.png" alt="프로필 이미지" />
|
||||
</div>
|
||||
<span class="rank-text">홍길동님 현재 <em>128</em>등 <span class="rank-change">(지난 주 대비 4↑)</span></span>
|
||||
<span class="rank-text">홍길동님 현재 <em>1250</em>등</span>
|
||||
</div>
|
||||
<div class="rank-toggle-wrap">
|
||||
<button class="btn-rank-toggle" type="button" aria-label="순위 상세 보기" aria-expanded="false" aria-controls="rankListPopup">
|
||||
<span class="ico-trophy" aria-hidden="true">
|
||||
<img class="lottie-fallback" src="/img/myclass/ico_trophy.gif" alt="" aria-hidden="true" />
|
||||
<span class="lottie-icon lottie-trophy" data-lottie-url="/img/myclass/ico_trophy.json"></span>
|
||||
</span>
|
||||
<span class="ico-chevron-down" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="rank-list-popup" id="rankListPopup" role="region" aria-label="순위 목록" hidden>
|
||||
<ul class="rank-list">
|
||||
<li class="rank-item rank-1"><span class="rank-medal rank-gold" aria-hidden="true"><img src="/img/myclass/ico_medal_gold.png" alt="금메달 아이콘" /></span><span class="rank-name">박길동 <small>(바론컨설턴트)</small></span></li>
|
||||
<li class="rank-item rank-2"><span class="rank-medal rank-silver" aria-hidden="true"><img src="/img/myclass/ico_medal_silver.png" alt="은메달 아이콘" /></span><span class="rank-name">홍길동 <small>(한맥기술)</small></span></li>
|
||||
<li class="rank-item rank-3"><span class="rank-medal rank-bronze" aria-hidden="true"><img src="/img/myclass/ico_medal_bronze.png" alt="동메달 아이콘" /></span><span class="rank-name">김철수 <small>(삼안)</small></span></li>
|
||||
<li class="rank-item rank-4"><span class="rank-num">4.</span><span class="rank-name">박철수 <small>(바른컨설턴트)</small></span></li>
|
||||
<li class="rank-item rank-5"><span class="rank-num">5.</span><span class="rank-name">김영희 <small>(한맥기술)</small></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
|
||||
<div class="page-intro" id="pageIntro">
|
||||
<?php
|
||||
// DB에서 현재 유저의 활성 목표 title 가져오기 (최신 분기 기준)
|
||||
$activeGoalTitle = null;
|
||||
if (isset($memberId) && $userCompany !== '') {
|
||||
try {
|
||||
$stmtActiveGoal = db_conn()->prepare(
|
||||
"SELECT g.title
|
||||
FROM edu_user_learning_goals u
|
||||
JOIN edu_learning_goals g ON u.goal_code = g.goal_code AND g.is_active = '1'
|
||||
WHERE u.member_id = ?
|
||||
AND u.sys_comp_code = ?
|
||||
AND u.is_active = '1'
|
||||
AND g.quarter = ?
|
||||
ORDER BY u.completed_date IS NULL DESC, u.completed_date ASC, u.goal_code ASC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmtActiveGoal->execute([(string)$memberId, (string)$userCompany, $currentQuarterCode]);
|
||||
$activeGoalTitle = $stmtActiveGoal->fetchColumn();
|
||||
} catch (Throwable $e) {
|
||||
error_log('[myclass.php] active goal title fetch error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php if ($mode === 'completed') : ?>
|
||||
<div class="page-title">
|
||||
<h3><?php echo $currentYear; ?>년도 <?php echo $currentQuarterNum; ?>분기의 첫번째 목표를 완성했어요!</h3>
|
||||
<p><em>다음 목표를 선택</em>해, 나만의 책장을 더 풍성하게 채워보세요.</p>
|
||||
</div>
|
||||
<?php elseif ($mode === 'additional') : ?>
|
||||
<div class="page-title">
|
||||
<h3><em>축하합니다.</em> <strong>[<?php echo htmlspecialchars($completedGoal['title'], ENT_QUOTES, 'UTF-8'); ?>]</strong>, 빛나는 책장을 완성했어요.</h3>
|
||||
<p><em>다음 책장</em>을 열어볼까요?</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="page-title page-title--goal" data-state="initial">
|
||||
<h3>
|
||||
<em><?php echo htmlspecialchars($userName, ENT_QUOTES, 'UTF-8') . ' ' . htmlspecialchars($userPosition, ENT_QUOTES, 'UTF-8'); ?></em>님,
|
||||
<strong><?php echo $currentYear; ?>년도 <?php echo $currentQuarterNum; ?>분기 학습 목표</strong>를 골라볼까요?
|
||||
</h3>
|
||||
<p class="page-title-deadline">
|
||||
<span class="deadline-highlight"><?php echo $daysDisplay; ?><small>(<?php echo $targetDateKr; ?>)</small></span>
|
||||
<span class="deadline-body">
|
||||
<?php if ($activeGoalTitle) : ?>
|
||||
현재 <strong>선택한 목표</strong>: <strong style="color:#f5b800;"><?php echo htmlspecialchars($activeGoalTitle, ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<?php else : ?>
|
||||
<strong>선택한 목표</strong>와 함께 배움을 채워가세요.
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-intro-mobile">
|
||||
<div class="page-title page-title--goal">
|
||||
<?php if ($isCompletionFlow) : ?>
|
||||
<h3><strong>다음 목표</strong>를 선택해 책장을 채워보세요.</h3>
|
||||
<?php else : ?>
|
||||
<h3><strong>선택한 목표</strong>와 함께 배움을 채워가세요.</h3>
|
||||
<?php endif; ?>
|
||||
<p class="page-title-deadline">D-64</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="myclass-inner">
|
||||
<div class="sub-title">
|
||||
<h4><?php echo substr($currentYear, 2); ?>년 <strong><?php echo $currentQuarterNum; ?></strong>분기)</h4>
|
||||
</div>
|
||||
|
||||
<section class="bookshelf goal" id="bookshelf" aria-label="학습 목표 선택">
|
||||
<div class="shelf-legs">
|
||||
<span class="shelf-leg shelf-leg-left"></span>
|
||||
<span class="shelf-leg shelf-leg-center"></span>
|
||||
<span class="shelf-leg shelf-leg-center"></span>
|
||||
<span class="shelf-leg shelf-leg-right"></span>
|
||||
</div>
|
||||
|
||||
<?php for ($row = 0; $row < 2; $row++) : ?>
|
||||
<div class="shelf-row">
|
||||
<ul class="goal-list">
|
||||
<?php for ($col = 0; $col < 3; $col++) : ?>
|
||||
<?php
|
||||
$idx = $row * 3 + $col;
|
||||
$goal = $goals[$idx];
|
||||
$goalId = $goal['id'];
|
||||
$isCompletedDB = in_array($goalId, $completedGoalIds, true);
|
||||
?>
|
||||
<li class="goal-item<?php echo ($isCompletedDB ? ' goal-item-completed-db' : ''); ?>" data-goal-id="<?php echo $goalId; ?>">
|
||||
<button class="goal-card" type="button" aria-label="<?php echo str_replace("\n", ' ', $goal['title']); ?> 목표 선택"
|
||||
<?php if ($isCompletedDB) : ?>
|
||||
disabled tabindex="-1" aria-disabled="true"
|
||||
<?php endif; ?>
|
||||
>
|
||||
<?php if ($isCompletedDB) : ?>
|
||||
<div class="goal-completed-preview" aria-hidden="true">
|
||||
<div class="goal-preview-header">
|
||||
<span class="goal-preview-badge">
|
||||
<img src="/img/myclass/ico_medal_completed.svg" alt="완득" />
|
||||
<span class="badge-text">완득</span>
|
||||
</span>
|
||||
<strong class="goal-preview-title"><?php echo str_replace("\n", '', $goal['title']); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="card-default-area">
|
||||
<div class="card-title-wrap">
|
||||
<div class="card-icon-wrap">
|
||||
<span class="card-icon" aria-hidden="true">
|
||||
<img class="lottie-fallback" src="/img/myclass/<?php echo $goal['gif']; ?>" alt="" aria-hidden="true" />
|
||||
<span class="lottie-icon lottie-<?php echo $goal['icon']; ?>" data-lottie-url="/img/myclass/<?php echo $goal['json']; ?>"></span>
|
||||
</span>
|
||||
</div>
|
||||
<strong class="card-title"><?php echo nl2br(htmlspecialchars($goal['title'], ENT_QUOTES, 'UTF-8')); ?></strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc"><?php echo htmlspecialchars($goal['desc'], ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
<div class="shelf-board"></div>
|
||||
<?php if ($row === 1) : ?><div class="shelf-board shelf-board-mobile"></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</section>
|
||||
|
||||
<?php if ($isCompletionFlow) : ?>
|
||||
<!--
|
||||
<div class="myclass-extra-actions" style="text-align:center; margin-bottom:28px;">
|
||||
<button type="button" class="btn-next-growth" id="btnSelectGoal">추가 목표 고르기</button>
|
||||
<a href="/skin/myclass_list.php" class="btn-next-growth" style="margin-left:8px;">학습하러 가기</a>
|
||||
</div>
|
||||
-->
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/_modal/goal-layer.php'); ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.MYCLASS_GOALS = <?php
|
||||
echo json_encode(array_map(function ($g) {
|
||||
return [
|
||||
'id' => (int)$g['id'],
|
||||
'title' => str_replace("\n", ' ', (string)$g['title']),
|
||||
'desc' => (string)$g['desc'],
|
||||
];
|
||||
}, $goals), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
?>;
|
||||
window.MYCLASS_GOAL_RECS = <?php
|
||||
echo json_encode($goalRecommendations, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
?>;
|
||||
window.MYCLASS_GOAL_BOOKS = <?php
|
||||
echo json_encode($goalBooksById, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
?>;
|
||||
window.MYCLASS_GOAL_CODE_MAP = <?php
|
||||
echo json_encode($goalCodeById, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
?>;
|
||||
window.MYCLASS_QUARTER_CODE = <?php
|
||||
echo json_encode($currentQuarterCode, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
?>;
|
||||
<?php if ($debugRecs) : ?>
|
||||
window.DEBUG_RECS_PAYLOAD = <?php echo json_encode($debugRecsPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
console.error('[debug_recs] ACTIVE');
|
||||
console.error('[debug_recs] MYCLASS_GOAL_CODE_MAP', window.MYCLASS_GOAL_CODE_MAP);
|
||||
console.error('[debug_recs] MYCLASS_GOAL_RECS', window.MYCLASS_GOAL_RECS);
|
||||
console.error('[debug_recs] MODAL_REC_ITEMS_FROM_PHP', <?php echo json_encode($modalRecItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>);
|
||||
console.error('[debug_recs] PAYLOAD', window.DEBUG_RECS_PAYLOAD);
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script src="/js/myclass.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
+637
@@ -0,0 +1,637 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/auth.php';
|
||||
edu_require_login();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css" />
|
||||
|
||||
<style>
|
||||
/* 마이페이지 콘텐츠 카드 삭제 버튼 숨김
|
||||
- 정책상 기능은 유지하되, 화면에는 노출하지 않는다. */
|
||||
.mypage .btn-remove-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 마이페이지 콘텐츠 빈 상태 문구 */
|
||||
.mypage .content-empty {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* 마이페이지 한줄소감
|
||||
- 사용자명/프로필 이미지 영역 제거 후 레이아웃 최소 보정 */
|
||||
.mypage .review-comment {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mypage .review-comment-text strong {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 한줄소감 빈 상태 문구 */
|
||||
.mypage .review-empty {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
list-style: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
function PG_set_suggest_detail(el) {
|
||||
// 1. 데이터 가져오기 (li 태그의 data- 속성들)
|
||||
const v_date = el.dataset.offerDate || '';
|
||||
const v_reason = el.dataset.offerReason || '';
|
||||
const v_return = el.dataset.offerReturn || '';
|
||||
|
||||
// 2. 기본 데이터 채워넣기
|
||||
document.getElementById('suggestDetailHeading').textContent = v_date;
|
||||
document.getElementById('suggestDetailReason').textContent = v_reason;
|
||||
|
||||
// 3. v_return 값 존재 여부에 따른 '게시 불가 사유' 영역 제어
|
||||
const bodySection = document.querySelector('.suggest-detail-body');
|
||||
const rejectField = document.getElementById('suggestDetailReject');
|
||||
|
||||
if (v_return && v_return.trim() !== '') {
|
||||
// 값이 존재할 때
|
||||
rejectField.textContent = v_return;
|
||||
bodySection.style.display = 'block';
|
||||
} else {
|
||||
// 값이 없을 때 (초기화 및 숨김)
|
||||
rejectField.textContent = '';
|
||||
bodySection.style.display = 'none';
|
||||
}
|
||||
|
||||
// 4. 모달 보이게 하기
|
||||
const modal = document.getElementById('suggestDetailModal');
|
||||
modal.hidden = false;
|
||||
}
|
||||
|
||||
//방법 1: 독립적인 함수로 만들기 (가장 추천)
|
||||
function closeSuggestDetail() {
|
||||
const modal = document.getElementById('suggestDetailModal');
|
||||
modal.hidden = true;
|
||||
|
||||
// 데이터 초기화 (필요시)
|
||||
document.getElementById('suggestDetailHeading').textContent = '';
|
||||
document.getElementById('suggestDetailReason').textContent = '';
|
||||
document.getElementById('suggestDetailReject').textContent = '';
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<?php
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_01.php';//개인정보
|
||||
require_once __DIR__ . '/../bbs/mypage_init_data_02.php';//제안하기 초기정보
|
||||
?>
|
||||
<?php
|
||||
$profileData = $profileData ?? [];
|
||||
$profileBadges = $profileBadges ?? [];
|
||||
|
||||
$profileName = $profileData['name'] ?? '사용자';
|
||||
$profileRankName = $profileData['rank_name'] ?? '';
|
||||
$profileWorkingComp = $profileData['working_comp'] ?? '';
|
||||
$profileBelongCompCd = $profileData['belong_comp_code'] ?? '';//소속회사 코드
|
||||
$profileBelongComp = $profileData['belong_comp_name'] ?? '';//소속회사 명
|
||||
$profileLevelLabel = $profileData['learning_level'] ?? 'Rookie';
|
||||
$profileLevelClass = $profileData['level_class'] ?? 'rookie';
|
||||
$profileLevelIcon = $profileData['level_icon'] ?? '/img/ico/ico_level_rookie.svg';
|
||||
$profileTotalMinutes = (int) ($profileData['total_minutes'] ?? 0);
|
||||
$profileImage = $profileData['profile_image'] ?? '/img/ico/ico_user.svg';
|
||||
|
||||
$badgeHat = $profileBadges['hat'] ?? null;
|
||||
$badgePencil = $profileBadges['pencil'] ?? null;
|
||||
$badgePick = $profileBadges['pick'] ?? null;
|
||||
?>
|
||||
<?php
|
||||
/* 제안하기 */
|
||||
$offerHistory = $offerHistory ?? [];
|
||||
$offerDefaultTypeCode = $offerDefaultTypeCode ?? 'OF10001';
|
||||
$offerDefaultStatusCode = $offerDefaultStatusCode ?? 'OF10001';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<!--
|
||||
========================================
|
||||
마이페이지 레이아웃 구조
|
||||
========================================
|
||||
[좌측] aside.mypage-sidebar : 프로필 + 컨텐츠 제안
|
||||
[우측] main.mypage-main : 학습 활동 + 시청/저장/소감 목록
|
||||
-->
|
||||
<div class="wrap mypage">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<div class="container">
|
||||
<div class="mypage-inner">
|
||||
<!--
|
||||
좌측 사이드바: 프로필 카드 + 컨텐츠 제안 폼
|
||||
수정 시: profile-card 내 이름, 직급, 학습레벨 등 변경
|
||||
-->
|
||||
<aside class="mypage-sidebar">
|
||||
<h2 class="mypage-title">마이페이지</h2>
|
||||
<div class="profile-area">
|
||||
<div class="profile-card">
|
||||
<div class="profile-photo-wrap">
|
||||
|
||||
<div class="profile-photo" id="profile-upload-trigger" style="cursor:pointer;"
|
||||
title="이미지를 클릭하시면 프로필사진 변경이 가능합니다">
|
||||
<div class="profile-photo-inner">
|
||||
<img id="profile-image" src="<?= $profileImage ?>">
|
||||
</div>
|
||||
<span class="ico-photo">
|
||||
<img src="/img/ico/ico_camera.svg" alt="카메라" />
|
||||
</span>
|
||||
</div>
|
||||
<input type="file" id="profile-file-input" accept="image/png, image/jpeg" style="display:none;">
|
||||
|
||||
<!-- PC 전용 프로필 배지 아이콘 -->
|
||||
<div class="profile-photo-badges" aria-hidden="true">
|
||||
<?php if (!empty($badgeHat)): ?>
|
||||
<span class="badge-item badge-item-hat">
|
||||
<img src="<?= htmlspecialchars($badgeHat['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgeHat['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePencil)): ?>
|
||||
<span class="badge-item badge-item-pencil">
|
||||
<img src="<?= htmlspecialchars($badgePencil['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePencil['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($badgePick)): ?>
|
||||
<span class="badge-item badge-item-pick">
|
||||
<img src="<?= htmlspecialchars($badgePick['img'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="<?= htmlspecialchars($badgePick['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>" />
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="profile-name">
|
||||
<em>
|
||||
<span class="ico-level">
|
||||
<img src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="학습레벨" />
|
||||
</span>
|
||||
<?= htmlspecialchars($profileName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</em>
|
||||
<span><?= htmlspecialchars($profileRankName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
|
||||
</p>
|
||||
<p class="profile-role"><?= htmlspecialchars($profileBelongComp, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</p>
|
||||
|
||||
<!-- badge-level master일때만 클래스명 추가 -->
|
||||
<!-- <div class="badge-level master"> -->
|
||||
<div class="badge-level <?= htmlspecialchars($profileLevelClass, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
id="mypage-level-box">
|
||||
|
||||
<span class="badge-level-title">학습레벨</span>
|
||||
|
||||
<span class="badge-level-value">
|
||||
<i class="ico-level">
|
||||
<img id="mypage-level-icon"
|
||||
src="<?= htmlspecialchars($profileLevelIcon, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
alt="학습레벨" />
|
||||
</i>
|
||||
<span id="mypage-level-label">
|
||||
<?= htmlspecialchars($profileLevelLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="ico-info-wrap">
|
||||
<i class="ico-info" aria-describedby="level-tooltip"></i>
|
||||
<span class="ico-info-tooltip" role="tooltip" id="level-tooltip">
|
||||
<span class="ico-info-tooltip-desc">누적된 학습 시간에 따라<br>4단계의 레벨이<br>자동 설정됩니다.</span>
|
||||
<ul class="ico-info-tooltip-list">
|
||||
<li><em>Master</em><span>40시간 이상</span></li>
|
||||
<li><em>Elite</em><span>20 ~ 40시간</span></li>
|
||||
<li><em>Learner</em><span>8 ~ 20시간</span></li>
|
||||
<li><em>Rookie</em><span>0 ~ 8시간</span></li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- MOBILE: 총 학습시간 버튼 (모바일에서만 표시, 클릭 시 활동 모달 오픈) -->
|
||||
<button class="mobile-total-time-btn" type="button" id="mobileActivityBtn" aria-label="총 학습시간 상세보기">
|
||||
<span>총 학습시간 <strong
|
||||
id="mobile-mypage-total-minutes"><?= number_format($profileTotalMinutes) ?></strong>분</span>
|
||||
<span class="mobile-total-time-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 컨텐츠 제안하기 Start -->
|
||||
<section class="suggest-section">
|
||||
<h3 class="suggest-title">컨텐츠 제안하기</h3>
|
||||
|
||||
<form id="offerForm" method="post" action="/ajax/insert_offer.php">
|
||||
<input type="hidden" name="type_code"
|
||||
value="<?= htmlspecialchars($offerDefaultTypeCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
<input type="hidden" name="status_code"
|
||||
value="<?= htmlspecialchars($offerDefaultStatusCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
|
||||
<div class="suggest-fields">
|
||||
<input type="url" class="suggest-input suggest-url" placeholder="URL" id="suggest-url"
|
||||
name="reference_url" />
|
||||
<textarea class="suggest-input suggest-reason" placeholder="추천이유" rows="4" id="suggest-reason"
|
||||
name="reason"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled>
|
||||
제안 보내기
|
||||
<i class="ico-arrow"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="suggest-status accordion">
|
||||
<button type="button" class="accordion-trigger" aria-expanded="false"
|
||||
aria-controls="suggest-status-content">
|
||||
<span class="accordion-title">제안현황</span>
|
||||
<i class="ico-chevron" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div id="suggest-status-content" class="accordion-content" hidden>
|
||||
<ul class="suggest-status-list">
|
||||
<?php if (!empty($offerHistory)): ?>
|
||||
<?php foreach ($offerHistory as $row): ?>
|
||||
<li style="cursor: pointer;" title="클릭 시 상세결과를 볼 수 있습니다." onclick="PG_set_suggest_detail(this);"
|
||||
data-offer-date="<?= htmlspecialchars($row['created_at_dot'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
data-offer-reason="<?= htmlspecialchars($row['reason'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
data-offer-return="<?= htmlspecialchars($row['reason_return'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||||
class="<?= !empty($row['is_consider']) ? 'consider' : '' ?>"
|
||||
data-offer-id="<?= htmlspecialchars($row['offer_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
<span class="suggest-date">
|
||||
<?= htmlspecialchars($row['created_at_dot'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
|
||||
<?php if (!empty($row['is_consider'])): ?>
|
||||
<span class="suggest-dots"></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="suggest-state">
|
||||
<?= htmlspecialchars($row['status_name'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<li>
|
||||
<span class="suggest-date">-</span>
|
||||
<span class="suggest-state">등록된 제안이 없습니다.</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제안 상세 레이어 -->
|
||||
<div class="suggest-detail-modal" id="suggestDetailModal" role="dialog" aria-modal="true"
|
||||
aria-labelledby="suggestDetailHeading" hidden>
|
||||
<div class="suggest-detail-panel">
|
||||
<div class="suggest-detail-header">
|
||||
<div class="suggest-detail-title">
|
||||
<p class="suggest-detail-heading" id="suggestDetailHeading"></p>
|
||||
<button onclick="closeSuggestDetail();" type="button" class="suggest-detail-close"
|
||||
id="suggestDetailClose" aria-label="닫기">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<dl class="suggest-detail-meta">
|
||||
<!-- <dt><span id="suggestDetailDate" class="suggest-detail-date"></span>추천 이유</dt> -->
|
||||
<dd><span id="suggestDetailDate" class=""></span><b>추천이유</b></dd>
|
||||
<dd id="suggestDetailReason"></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="suggest-detail-body">
|
||||
<p class="suggest-detail-body-title" style="color:#cf6800;">게시불가 사유</p>
|
||||
<p class="suggest-detail-reject" id="suggestDetailReject"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 컨텐츠 제안하기 End -->
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!--
|
||||
우측 메인: 학습 활동 통계 + 시청/저장/소감 목록
|
||||
학습 시간 수정: total-time-text의 strong, gauge-fill의 width 값
|
||||
-->
|
||||
<main class="mypage-main">
|
||||
<section class="activity-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin"></i>
|
||||
나의 학습 활동
|
||||
<div class="select-box">
|
||||
<label class="year-badge" style="display: none;"></label>
|
||||
<select id="mypage-year-select">
|
||||
<option>년</option>
|
||||
</select>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="total-time-area">
|
||||
<div class="total-time-inner">
|
||||
<div class="total-time">
|
||||
<p class="total-time-text">
|
||||
총 학습시간 <strong id="mypage-total-minutes"></strong>분
|
||||
</p>
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" id="mypage-total-gauge" style="width:50%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="total-average">전체평균 <em id="mypage-avg-minutes">0</em>분</span>
|
||||
</div>
|
||||
|
||||
<ul class="activity-list">
|
||||
<li class="activity-item gauge" data-category="CA10001">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">마이클래스</span>
|
||||
<span class="activity-value"><em></em>분 (0%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 33%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10002">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">온보딩</span>
|
||||
<span class="activity-value"><em></em>분 (80%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 80%"></div>
|
||||
<p class="activity-note" style="display:none;">
|
||||
<span class="activity-note-num">①</span> 필수 시청: <span class="onboarding-due-date">26.01.14</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item gauge" data-category="CA10003">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">법정교육</span>
|
||||
<span class="activity-value"><em></em>분 (20%)</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">개</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10004">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">리더십</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 40%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10005">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">인사이트</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 60%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="activity-item tag" data-category="CA10006">
|
||||
<div class="activity-head">
|
||||
<span class="activity-label">비즈트렌드</span>
|
||||
<span class="activity-value"><em></em>분</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 20%;"></div>
|
||||
</div>
|
||||
<div class="gauge-value">
|
||||
<span class="gauge-start">0</span>
|
||||
<span class="gauge-separator">·</span>
|
||||
<span class="gauge-end">분</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="mylist-area">
|
||||
<!-- MOBILE: 2탭 네비게이션 (시청완료 <> 슬라이더 + 한줄소감) -->
|
||||
<div class="mobile-list-tab-header">
|
||||
<button class="mobile-list-tab active" type="button" id="mobileTabContent" data-mobile-tab="content"
|
||||
aria-selected="true">
|
||||
<span class="mobile-tab-arrow mobile-tab-prev" aria-label="이전" aria-hidden="true">‹</span>
|
||||
<span class="mobile-tab-label">시청완료</span>
|
||||
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">›</span>
|
||||
</button>
|
||||
<button class="mobile-list-tab" type="button" id="mobileTabReview" data-mobile-tab="review"
|
||||
aria-selected="false">
|
||||
<span class="ico-mobile-pencil" aria-hidden="true"></span>
|
||||
한줄 소감
|
||||
</button>
|
||||
</div>
|
||||
<!-- 콘텐츠 탭 영역 -->
|
||||
<div class="content-tab-wrap">
|
||||
<div class="content-tabs" role="tablist">
|
||||
<button type="button" class="content-tab active" role="tab" aria-selected="true"
|
||||
aria-controls="panel-watching" id="tab-watching" data-tab="watching">
|
||||
시청중인 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false"
|
||||
aria-controls="panel-completed" id="tab-completed" data-tab="completed">
|
||||
시청완료 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
<button type="button" class="content-tab" role="tab" aria-selected="false" aria-controls="panel-saved"
|
||||
id="tab-saved" data-tab="saved">
|
||||
저장한 콘텐츠 <span class="count"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-panels">
|
||||
<!-- 시청중인 콘텐츠 -->
|
||||
<section class="content-section content-panel active" id="panel-watching" role="tabpanel"
|
||||
aria-labelledby="tab-watching" data-panel="watching">
|
||||
<ul class="content-grid" id="mypage-watching-list"></ul>
|
||||
</section>
|
||||
|
||||
<!-- 시청완료 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-completed" role="tabpanel"
|
||||
aria-labelledby="tab-completed" data-panel="completed">
|
||||
<ul class="content-grid" id="mypage-completed-list"></ul>
|
||||
</section>
|
||||
<!-- 저장한 콘텐츠 -->
|
||||
<section class="content-section content-panel" id="panel-saved" role="tabpanel"
|
||||
aria-labelledby="tab-saved" data-panel="saved">
|
||||
<ul class="content-grid" id="mypage-saved-list"></ul>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 한줄 소감 -->
|
||||
<section class="review-section">
|
||||
<h3 class="section-title">
|
||||
<i class="ico-pin-list" aria-hidden="true"></i>
|
||||
한줄 소감 <span class="count" id="mypage-review-count">0</span>
|
||||
</h3>
|
||||
|
||||
<ul class="review-list" id="mypage-review-list"></ul>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- MOBILE: 하단 목록 카운트 + 컨텐츠 제안 바 (모바일에서만 표시) -->
|
||||
<nav class="mobile-bottom-nav" aria-label="모바일 콘텐츠 탐색">
|
||||
<button class="mobile-nav-item mobile-nav-item--suggest" type="button" id="mobileSuggestBtn">
|
||||
<span class="mobile-nav-ico mobile-nav-ico--send" aria-hidden="true"></span>
|
||||
<span class="mobile-nav-label">컨텐츠 제안하기</span>
|
||||
<span class="mobile-nav-chevron mobile-nav-chevron--right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
|
||||
<!-- MOBILE: 학습 활동 팝업 모달 -->
|
||||
<div class="mobile-activity-modal" id="mobileActivityModal" role="dialog" aria-modal="true"
|
||||
aria-labelledby="mobileModalTitle" hidden>
|
||||
<div class="modal-backdrop" id="mobileActivityBackdrop"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<p class="modal-title" id="mobileModalTitle">
|
||||
<span class="titile-label">총 학습시간</span> <strong>340</strong><span>분</span>
|
||||
</p>
|
||||
<div class="modal-gauge-wrap">
|
||||
<div class="gauge-bar">
|
||||
<div class="gauge-fill" style="width: 63%"></div>
|
||||
</div>
|
||||
<span class="modal-average-badge">전체 평균</span>
|
||||
</div>
|
||||
<button class="modal-close-btn" type="button" id="mobileActivityClose" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="modal-activity-list">
|
||||
<li>
|
||||
<span class="modal-label">마이클래스</span>
|
||||
<span class="modal-value modal-value--green"><em>74</em>분(33%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">온보딩 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i>
|
||||
필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>110</em>분(80%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">법정교육 <span class="modal-required"><i class="ico-info-xs" aria-hidden="true"></i>
|
||||
필수 시청 <em>26.01.14</em></span></span>
|
||||
<span class="modal-value modal-value--green"><em>30</em>분(20%)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">리더십</span>
|
||||
<span class="modal-value modal-value--brown"><em>60</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">인사이트</span>
|
||||
<span class="modal-value modal-value--brown"><em>43</em>분</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="modal-label">비즈트렌드</span>
|
||||
<span class="modal-value modal-value--brown"><em>23</em>분</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE: 컨텐츠 제안 바텀시트 -->
|
||||
<div class="mobile-suggest-sheet" id="mobileSuggestSheet" role="dialog" aria-modal="true" hidden>
|
||||
<div class="sheet-backdrop" id="mobileSuggestBackdrop"></div>
|
||||
<div class="sheet-panel">
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-header-ico" aria-hidden="true"></span>
|
||||
<p class="sheet-title">컨텐츠 제안하기</p>
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="suggest-fields">
|
||||
<input type="url" class="suggest-input sheet-url" placeholder="URL" id="sheet-url" />
|
||||
<textarea class="suggest-input sheet-reason" placeholder="추천이유" rows="5" id="sheet-reason"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn-sheet-confirm" id="btnSheetConfirm" disabled>확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 마이페이지 전용 스크립트: 제안 폼 버튼 활성화, 제안현황 아코디언 -->
|
||||
<script src="/js/learning/config.js" defer></script>
|
||||
<script src="/js/learning/markers.js" defer></script>
|
||||
<script src="/js/learning/modal.js" defer></script>
|
||||
<script src="/js/bridges/learning-modal-bridge.js" defer></script>
|
||||
<script>
|
||||
window.puzzleConfig = {
|
||||
...(window.puzzleConfig || {}),
|
||||
COMPLETION_MODE: 'COMPLETED',
|
||||
CURRENT_MEMBER_ID: <?php echo json_encode((string) ($_SESSION['member_id'] ?? ''), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP); ?>,
|
||||
};
|
||||
</script>
|
||||
<script src="/js/puzzle-onboarding.js" defer></script>
|
||||
<script src="/js/bridges/onboarding-modal-bridge.js" defer></script>
|
||||
<script src="/js/main/Videomodalmanager.js" defer></script>
|
||||
<script src="/js/mypage.js" defer></script>
|
||||
<!-- ERP기획 : 26.03.20 moon -->
|
||||
<script src="/js/apply_page/add_mypage.js?v=321321" defer></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../bbs/auth.php";
|
||||
edu_require_login();
|
||||
|
||||
require_once __DIR__ . "/../bbs/db_conn.php";
|
||||
$pdo = db_conn();
|
||||
|
||||
$memberId = (string)($_SESSION['member_id'] ?? '');
|
||||
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
|
||||
|
||||
if ($memberId === '' || $sysCompCode === '') {
|
||||
http_response_code(401);
|
||||
exit('로그인 정보가 유효하지 않습니다. 다시 로그인해 주세요.');
|
||||
}
|
||||
|
||||
// 사용자 정보 및 join_date 조회
|
||||
$stmtUser = $pdo->prepare("
|
||||
SELECT name, rank_name, join_date, sys_comp_code
|
||||
FROM edu_users
|
||||
WHERE member_id = ?
|
||||
AND sys_comp_code = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtUser->execute([$memberId, $sysCompCode]);
|
||||
$userRow = $stmtUser->fetch() ?: [];
|
||||
|
||||
$userName = $userRow['name'] ?? '사용자';
|
||||
$userRank = $userRow['rank_name'] ?? '';
|
||||
$sysCompCode = (string)($userRow['sys_comp_code'] ?? $sysCompCode);
|
||||
|
||||
// join_date 기준 14일 마감 계산
|
||||
$today = new DateTime(date('Y-m-d'));
|
||||
try {
|
||||
$joinBase = !empty($userRow['join_date'])
|
||||
? new DateTime(date('Y-m-d', strtotime($userRow['join_date'])))
|
||||
: clone $today;
|
||||
} catch (Exception $e) {
|
||||
$joinBase = clone $today;
|
||||
}
|
||||
|
||||
$deadline = (clone $joinBase)->modify('+14 days');
|
||||
$dDay = ($today <= $deadline) ? (int)$today->diff($deadline)->days : 0;
|
||||
$deadlineStr = $deadline->format('n월 j일');
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . "/_include/_head.php") ?>
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css" />
|
||||
|
||||
<!-- <style>
|
||||
/* puzzle-onboarding.js 미구현 동안 스크롤 임시 허용 */
|
||||
html, body { overflow: auto !important; height: auto !important; }
|
||||
.wrap.onboarding { position: static !important; overflow: visible !important; }
|
||||
.wrap.onboarding .container { position: static !important; overflow: visible !important; }
|
||||
</style> -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap onboarding">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<div class="puzzle-wrap">
|
||||
<div class="page-title">
|
||||
<h3>
|
||||
<span><em><?= htmlspecialchars(trim($userName . ' ' . $userRank), ENT_QUOTES, 'UTF-8') ?></em>님,</span> <?= $dDay>0?'입사를 환영합니다.':''?>
|
||||
</h3>
|
||||
<p>
|
||||
<em><?= $dDay>0?'D-'.$dDay:'' ?><small><?= $dDay>0?'('.htmlspecialchars($deadlineStr, ENT_QUOTES, 'UTF-8').')':'' ?></small><?= $dDay>0?'까지':''?> 필수 콘텐츠</em> 시청을
|
||||
완료해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="puzzle-area">
|
||||
<div class="puzzle-board" id="puzzleBoard">
|
||||
<!-- SVG가 여기에 동적으로 삽입됩니다 -->
|
||||
|
||||
<p class="img-box family">
|
||||
<img src="/img/onboarding/img_obj_01.png" loading="lazy" />
|
||||
</p>
|
||||
|
||||
<p class="img-box hanmac">
|
||||
<img src="/img/onboarding/img_obj_02.png" loading="lazy" />
|
||||
</p>
|
||||
|
||||
<p class="img-box value">
|
||||
<img src="/img/onboarding/img_obj_03.png" loading="lazy" />
|
||||
</p>
|
||||
|
||||
<p class="img-box company">
|
||||
<img src="/img/onboarding/img_obj_04.png" loading="lazy" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 1. 온보딩 콘텐츠 조회 (category_group 기준)
|
||||
$onboardingGroups = [
|
||||
'CA200O01', 'CA200O02', 'CA200O03', 'CA200O04', 'CA200O05',
|
||||
'CA200O06', 'CA200O07', 'CA200O08', 'CA200O09', 'CA200O10',
|
||||
];
|
||||
$placeholders = implode(',', array_fill(0, count($onboardingGroups), '?'));
|
||||
|
||||
// edu_content_histories 테이블 존재 여부에 따라 쿼리 분기
|
||||
$useContentHistories = true;
|
||||
try {
|
||||
$checkTable = $pdo->query("SHOW TABLES LIKE 'edu_content_histories'");
|
||||
if ($checkTable->rowCount() === 0) {
|
||||
$useContentHistories = false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$useContentHistories = false;
|
||||
}
|
||||
|
||||
if ($useContentHistories) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
c.*,
|
||||
COALESCE(lh.watch_tm, 0) AS watch_tm,
|
||||
COALESCE(lh.content_tm, 0) AS content_tm,
|
||||
lh.completed_at AS lh_completed_at,
|
||||
CASE
|
||||
WHEN ch.content_id IS NULL THEN 'none'
|
||||
WHEN ch.completed_at IS NOT NULL THEN 'completed'
|
||||
ELSE 'in_progress'
|
||||
END AS learning_status
|
||||
FROM edu_contents c
|
||||
LEFT JOIN edu_learning_histories lh
|
||||
ON lh.content_id = c.content_id
|
||||
AND lh.member_id = ?
|
||||
AND lh.sys_comp_code = ?
|
||||
LEFT JOIN edu_content_histories ch
|
||||
ON ch.content_id = c.content_id
|
||||
AND ch.member_id = ?
|
||||
AND ch.sys_comp_code = ?
|
||||
WHERE c.category_group IN ($placeholders)
|
||||
ORDER BY FIELD(c.category_group, 'CA200O01','CA200O02','CA200O03','CA200O04','CA200O05','CA200O06','CA200O07','CA200O08','CA200O09','CA200O10'), COALESCE(NULLIF(c.sort_order, 0), 9999), c.content_id
|
||||
");
|
||||
$stmt->execute(array_merge([$memberId, $sysCompCode, $memberId, $sysCompCode], $onboardingGroups));
|
||||
$rows = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
// edu_content_histories JOIN 실패 시 fallback
|
||||
$useContentHistories = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$useContentHistories) {
|
||||
// fallback: edu_learning_histories만 사용
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
c.*,
|
||||
COALESCE(lh.watch_tm, 0) AS watch_tm,
|
||||
COALESCE(lh.content_tm, 0) AS content_tm,
|
||||
lh.completed_at AS lh_completed_at,
|
||||
CASE
|
||||
WHEN lh.content_id IS NULL THEN 'none'
|
||||
WHEN lh.completed_at IS NOT NULL THEN 'completed'
|
||||
ELSE 'in_progress'
|
||||
END AS learning_status
|
||||
FROM edu_contents c
|
||||
LEFT JOIN edu_learning_histories lh
|
||||
ON lh.content_id = c.content_id
|
||||
AND lh.member_id = ?
|
||||
AND lh.sys_comp_code = ?
|
||||
WHERE c.category_group IN ($placeholders)
|
||||
ORDER BY FIELD(c.category_group, 'CA200O01','CA200O02','CA200O03','CA200O04','CA200O05','CA200O06','CA200O07','CA200O08','CA200O09','CA200O10'), COALESCE(NULLIF(c.sort_order, 0), 9999), c.content_id
|
||||
");
|
||||
$stmt->execute(array_merge([$memberId, $sysCompCode], $onboardingGroups));
|
||||
$rows = $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// 2. category_group -> 퍼즐 피스/영역 매핑
|
||||
// area: family(소개), hanmac(DX), value(가치), company(회사생활)
|
||||
$categoryMap = [
|
||||
'CA200O01' => ['pieceId' => 1, 'group' => 3, 'area' => 'value'],
|
||||
'CA200O02' => ['pieceId' => 2, 'group' => 2, 'area' => 'hanmac'],
|
||||
'CA200O03' => ['pieceId' => 3, 'group' => 2, 'area' => 'hanmac'],
|
||||
'CA200O04' => ['pieceId' => 4, 'group' => 2, 'area' => 'hanmac'],
|
||||
'CA200O05' => ['pieceId' => 5, 'group' => 2, 'area' => 'hanmac'],
|
||||
'CA200O06' => ['pieceId' => 6, 'group' => 3, 'area' => 'value'],
|
||||
'CA200O07' => ['pieceId' => 7, 'group' => 4, 'area' => 'company'],
|
||||
'CA200O08' => ['pieceId' => 8, 'group' => 4, 'area' => 'company'],
|
||||
'CA200O09' => ['pieceId' => 9, 'group' => 1, 'area' => 'family'],
|
||||
'CA200O10' => ['pieceId' => 10, 'group' => 1, 'area' => 'family'],
|
||||
];
|
||||
|
||||
// ✅ 온보딩 챕터 제목을 edu_codes 테이블에서 동적으로 로드
|
||||
// 관리자가 edu_codes에서 code_name을 수정하면 자동으로 퍼즐 제목이 변경됨
|
||||
$chapterNameMap = [];
|
||||
try {
|
||||
$stmtCodes = $pdo->prepare("
|
||||
SELECT base_code, code_name
|
||||
FROM edu_codes
|
||||
WHERE group_code = 'CA200'
|
||||
AND base_code IN ('CA200O01','CA200O02','CA200O03','CA200O04','CA200O05','CA200O06','CA200O07','CA200O08','CA200O09','CA200O10')
|
||||
ORDER BY base_code
|
||||
");
|
||||
$stmtCodes->execute();
|
||||
$codeRows = $stmtCodes->fetchAll();
|
||||
|
||||
foreach ($codeRows as $codeRow) {
|
||||
$baseCode = (string)($codeRow['base_code'] ?? '');
|
||||
$codeName = trim((string)($codeRow['code_name'] ?? ''));
|
||||
if ($baseCode && $codeName) {
|
||||
$chapterNameMap[$baseCode] = $codeName;
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 학습 부분은 진행되어야 하므로 조용히 실패 처리
|
||||
error_log('[onboarding.php] edu_codes 조회 실패: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 3. 같은 category_group은 같은 모달(챕터)로 묶고 lessons에 1/2/3차시 누적
|
||||
$chapterMap = [];
|
||||
foreach ($rows as $idx => $row) {
|
||||
$categoryGroup = $row['category_group'] ?? '';
|
||||
$mapped = $categoryMap[$categoryGroup] ?? null;
|
||||
|
||||
// description 컬럼명 편차 대응
|
||||
$description = trim((string)($row['description'] ?? $row['description1'] ?? $row['description_1'] ?? $row['content_desc'] ?? $row['content_desc1'] ?? $row['content_description'] ?? $row['content_description1'] ?? ''));
|
||||
$description2 = trim((string)($row['description2'] ?? $row['description_2'] ?? $row['content_desc2'] ?? $row['content_description2'] ?? ''));
|
||||
|
||||
// 미정의 category_group은 뒤 순번으로 안전 처리
|
||||
$pid = $mapped['pieceId'] ?? ($idx + 1);
|
||||
$group = $mapped['group'] ?? 1;
|
||||
$area = $mapped['area'] ?? 'family';
|
||||
|
||||
if (!isset($chapterMap[$categoryGroup])) {
|
||||
$chapterMap[$categoryGroup] = [
|
||||
'id' => 0, // 아래에서 순차 부여
|
||||
// ✅ 챕터 제목은 고정된 chapterNameMap에서 가져옴 (콘텐츠 title이 아님)
|
||||
'name' => $chapterNameMap[$categoryGroup] ?? str_replace('[온보딩] ', '', (string)$row['title']),
|
||||
'pieceId' => $pid,
|
||||
'type' => 'youtube',
|
||||
'group' => $group,
|
||||
'category_group' => $categoryGroup,
|
||||
'area' => $area,
|
||||
'lessons' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$lessonNo = (int)($row['sort_order'] ?? 0);
|
||||
if ($lessonNo <= 0) {
|
||||
$lessonNo = count($chapterMap[$categoryGroup]['lessons']) + 1;
|
||||
}
|
||||
|
||||
// learning_status: none(미진행), in_progress(학습중), completed(학습완료)
|
||||
$learningStatus = $row['learning_status'] ?? 'none';
|
||||
|
||||
$chapterMap[$categoryGroup]['lessons'][] = [
|
||||
'content_id' => $row['content_id'],
|
||||
// 북마크/댓글은 edu_contents.content_id와 동일한 문자열 키를 사용한다.
|
||||
'bookmark_content_id' => $row['content_id'],
|
||||
'comment_content_id' => $row['content_id'],
|
||||
'title' => $row['title'],
|
||||
'label' => str_replace('[온보딩] ', '', $row['title']),
|
||||
'description' => $description,
|
||||
'description2' => $description2,
|
||||
'url' => $row['content_url'],
|
||||
'completed' => ($learningStatus === 'completed'),
|
||||
'learning_status' => $learningStatus,
|
||||
'watch_tm' => (int)$row['watch_tm'],
|
||||
'content_tm' => (int)$row['content_tm'],
|
||||
];
|
||||
}
|
||||
|
||||
// 4. 북마크 상태 로드 (is_active: 1=표시, 0=미표시)
|
||||
$bookmarkIds = [];
|
||||
foreach ($chapterMap as $chapterItem) {
|
||||
foreach (($chapterItem['lessons'] ?? []) as $lessonItem) {
|
||||
$bookmarkId = trim((string)($lessonItem['bookmark_content_id'] ?? ''));
|
||||
if ($bookmarkId !== '') {
|
||||
$bookmarkIds[] = $bookmarkId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$bookmarkIds = array_values(array_unique($bookmarkIds));
|
||||
|
||||
$wishlistMap = [];
|
||||
if (!empty($bookmarkIds)) {
|
||||
$wishlistPlaceholders = implode(',', array_fill(0, count($bookmarkIds), '?'));
|
||||
$stmtWishlist = $pdo->prepare(
|
||||
"SELECT content_id, is_active
|
||||
FROM edu_content_wishlist
|
||||
WHERE member_id = ?
|
||||
AND sys_comp_code = ?
|
||||
AND content_id IN ($wishlistPlaceholders)"
|
||||
);
|
||||
$stmtWishlist->execute(array_merge([$memberId, $sysCompCode], $bookmarkIds));
|
||||
$wishlistRows = $stmtWishlist->fetchAll();
|
||||
|
||||
foreach ($wishlistRows as $wishlistRow) {
|
||||
$wishlistMap[(string)$wishlistRow['content_id']] = ((string)($wishlistRow['is_active'] ?? '0') === '1');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($chapterMap as &$chapterItem) {
|
||||
foreach ($chapterItem['lessons'] as &$lessonItem) {
|
||||
$bookmarkId = trim((string)($lessonItem['bookmark_content_id'] ?? ''));
|
||||
$lessonItem['is_bookmarked'] = ($bookmarkId !== '' && !empty($wishlistMap[$bookmarkId]));
|
||||
}
|
||||
unset($lessonItem);
|
||||
}
|
||||
unset($chapterItem);
|
||||
|
||||
// category_group 정의 순서대로 챕터 정렬 + id 재부여
|
||||
$puzzleData = [];
|
||||
foreach ($onboardingGroups as $code) {
|
||||
if (!isset($chapterMap[$code])) {
|
||||
continue;
|
||||
}
|
||||
$chapterMap[$code]['id'] = count($puzzleData) + 1;
|
||||
$puzzleData[] = $chapterMap[$code];
|
||||
}
|
||||
$puzzleJson = json_encode($puzzleData, JSON_UNESCAPED_UNICODE);
|
||||
?>
|
||||
|
||||
<script>
|
||||
// 1. CONFIG 설정
|
||||
window.puzzleConfig = {
|
||||
COMPLETION_MODE: "COMPLETED",
|
||||
CURRENT_MEMBER_ID: <?= json_encode($memberId, JSON_UNESCAPED_UNICODE) ?>,
|
||||
};
|
||||
|
||||
// 2. 챕터 기반 데이터 설정 (DB에서 동적 로드)
|
||||
window.puzzleChapterData = <?= $puzzleJson ?>;
|
||||
</script>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/puzzle-onboarding.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,264 @@
|
||||
<!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 onboarding">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<div class="puzzle-wrap">
|
||||
<div class="page-title">
|
||||
<h3>
|
||||
<span><em>홍길동 선임연구원</em>님,</span> 입사를 환영합니다.
|
||||
</h3>
|
||||
<p>
|
||||
<em>D-14<small>(1월 14일)</small>까지 필수 콘텐츠</em> 시청을
|
||||
완료해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="puzzle-area">
|
||||
<div class="puzzle-board" id="puzzleBoard">
|
||||
<!-- SVG가 여기에 동적으로 삽입됩니다 -->
|
||||
<p class="img-box family">
|
||||
<img src="/img/onboarding/img_obj_01.png" />
|
||||
</p>
|
||||
<p class="tit-box family">가족사 소개</p>
|
||||
|
||||
<p class="img-box hanmac">
|
||||
<img src="/img/onboarding/img_obj_02.png" />
|
||||
</p>
|
||||
<p class="tit-box hanmac">한맥가족의 DX</p>
|
||||
|
||||
<p class="img-box value">
|
||||
<img src="/img/onboarding/img_obj_03.png" />
|
||||
</p>
|
||||
<p class="tit-box value">가치공유</p>
|
||||
|
||||
<p class="img-box company">
|
||||
<img src="/img/onboarding/img_obj_04.png" />
|
||||
</p>
|
||||
<p class="tit-box company">회사생활</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 1. CONFIG 설정 (puzzle-onboarding.js 로드 전에!)
|
||||
window.puzzleConfig = {
|
||||
COMPLETION_MODE: "COMPLETED", // "COMPLETED" 또는 "FINISH"
|
||||
};
|
||||
|
||||
// 2. 콘텐츠 데이터 설정
|
||||
window.puzzleChapterData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "한맥가족 소개 및 경영이념",
|
||||
pieceId: 9,
|
||||
type: "youtube",
|
||||
group: 1,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "한맥가족의 역사",
|
||||
url: "KWKJbTgtkrk",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "삼안 회사 소개",
|
||||
pieceId: 10,
|
||||
type: "youtube",
|
||||
group: 1,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "삼안 회사 개요",
|
||||
url: "HugaMHZRBC8",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "왜 다윈인인가",
|
||||
pieceId: 1,
|
||||
type: "youtube",
|
||||
group: 3,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#001",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#002",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#003",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#004",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#001",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#002",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#003",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#004",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "새로운 시대를 준비하는 우리",
|
||||
pieceId: 2,
|
||||
type: "file",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "디지털 전환의 시대",
|
||||
url: "./files/education.pdf",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "기술개발센터 소개",
|
||||
pieceId: 3,
|
||||
type: "youtube",
|
||||
group: 2,
|
||||
lessons: [
|
||||
{
|
||||
label: "기술개발센터 역할",
|
||||
url: "T3pkeUl5fT4",
|
||||
completed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "건설산업의 디지털 전환",
|
||||
pieceId: 4,
|
||||
type: "youtube",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "건설 DX 개요",
|
||||
url: "T3pkeUl5fT4",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "상용 소프트웨어 소개",
|
||||
pieceId: 5,
|
||||
type: "youtube",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "주요 S/W 플랫폼",
|
||||
url: "T3pkeUl5fT4",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "축적의 시간",
|
||||
pieceId: 6,
|
||||
type: "youtube",
|
||||
group: 3,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "축적의 시간 - 착각의 시간",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "축적의 시간 - 축적에서 길을 찾다",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "축적의 시간 2 - 천재는 잊어라",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "축적의 시간2 - 유령이 된 리더들",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "회사생활 안내 (경력)",
|
||||
pieceId: 7,
|
||||
type: "youtube",
|
||||
group: 4,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "경력사원 온보딩",
|
||||
url: "Py7nutVN53s",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "회사생활 안내 (신규)",
|
||||
pieceId: 8,
|
||||
type: "youtube",
|
||||
group: 4,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "신규입사자 가이드",
|
||||
url: "Py7nutVN53s",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<script src="/js/puzzle-onboarding.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,266 @@
|
||||
<!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 onboarding">
|
||||
<?php include(__DIR__ . "/_include/_header.php") ?>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
<div class="puzzle-wrap">
|
||||
<div class="page-title">
|
||||
<h3>
|
||||
<em>온보딩 필수 콘텐츠</em> 시청을 완료하셨습니다.
|
||||
</h3>
|
||||
<p class="fw-b">
|
||||
필요할 땐 <em>언제든</em> 다시 볼 수 있어요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="puzzle-area">
|
||||
<div class="puzzle-board" id="puzzleBoard">
|
||||
<!-- SVG가 여기에 동적으로 삽입됩니다 -->
|
||||
|
||||
<p class="img-box family">
|
||||
<img src="/img/onboarding/img_obj_01.png" />
|
||||
</p>
|
||||
<p class="tit-box family">가족사 소개</p>
|
||||
|
||||
<p class="img-box hanmac">
|
||||
<img src="/img/onboarding/img_obj_02.png" />
|
||||
</p>
|
||||
<p class="tit-box hanmac">한맥가족의 DX</p>
|
||||
|
||||
<p class="img-box value">
|
||||
<img src="/img/onboarding/img_obj_03.png" />
|
||||
</p>
|
||||
<p class="tit-box value">가치공유</p>
|
||||
|
||||
<p class="img-box company">
|
||||
<img src="/img/onboarding/img_obj_04.png" />
|
||||
</p>
|
||||
<p class="tit-box company">회사생활</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- // container -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 전역 변수로 콘텐츠 데이터 설정 (PuzzleManager가 자동으로 읽음)
|
||||
// 1. CONFIG 설정 (puzzle-onboarding.js 로드 전에!)
|
||||
window.puzzleConfig = {
|
||||
COMPLETION_MODE: "FINISH", // "COMPLETED" 또는 "FINISH"
|
||||
};
|
||||
// 2. 콘텐츠 데이터 설정
|
||||
window.puzzleChapterData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "한맥가족 소개 및 경영이념",
|
||||
pieceId: 9,
|
||||
type: "youtube",
|
||||
group: 1,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "한맥가족의 역사",
|
||||
url: "KWKJbTgtkrk",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "삼안 회사 소개",
|
||||
pieceId: 10,
|
||||
type: "youtube",
|
||||
group: 1,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "삼안 회사 개요",
|
||||
url: "HugaMHZRBC8",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "왜 다윈인인가",
|
||||
pieceId: 1,
|
||||
type: "youtube",
|
||||
group: 3,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#001",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#002",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#003",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 1강 소통_#004",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#001",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#002",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#003",
|
||||
url: "OXTYn3JkkCQ",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "왜 다윈인가? - 2강 통섭_#004",
|
||||
url: "oVoZLxi62Zg",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "새로운 시대를 준비하는 우리",
|
||||
pieceId: 2,
|
||||
type: "file",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "디지털 전환의 시대",
|
||||
url: "./files/education.pdf",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "기술개발센터 소개",
|
||||
pieceId: 3,
|
||||
type: "youtube",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "기술개발센터 역할",
|
||||
url: "T3pkeUl5fT4",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "건설산업의 디지털 전환",
|
||||
pieceId: 4,
|
||||
type: "youtube",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "건설 DX 개요",
|
||||
url: "T3pkeUl5fT4",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "상용 소프트웨어 소개",
|
||||
pieceId: 5,
|
||||
type: "youtube",
|
||||
group: 2,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "주요 S/W 플랫폼",
|
||||
url: "T3pkeUl5fT4",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "축적의 시간",
|
||||
pieceId: 6,
|
||||
type: "youtube",
|
||||
group: 3,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "축적의 시간 - 착각의 시간",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "축적의 시간 - 축적에서 길을 찾다",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "축적의 시간 2 - 천재는 잊어라",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
label: "축적의 시간2 - 유령이 된 리더들",
|
||||
url: "8MugD6Cwhl8",
|
||||
completed: true,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "회사생활 안내 (경력)",
|
||||
pieceId: 7,
|
||||
type: "youtube",
|
||||
group: 4,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "경력사원 온보딩",
|
||||
url: "Py7nutVN53s",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "회사생활 안내 (신규)",
|
||||
pieceId: 8,
|
||||
type: "youtube",
|
||||
group: 4,
|
||||
completed: true,
|
||||
lessons: [
|
||||
{
|
||||
label: "신규입사자 가이드",
|
||||
url: "Py7nutVN53s",
|
||||
completed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<script src="/js/puzzle-onboarding.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bbs/auth.php';
|
||||
edu_require_login();
|
||||
|
||||
require_once __DIR__ . '/../bbs/db_conn.php';
|
||||
|
||||
$q = trim((string)($_GET['q'] ?? ''));
|
||||
$results = [];
|
||||
|
||||
function h(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function parse_youtube_id(string $raw): string
|
||||
{
|
||||
if (preg_match('/(?:v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
if (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
|
||||
return $raw;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function make_thumb_url(string $thumb, string $contentUrl): string
|
||||
{
|
||||
$thumb = trim($thumb);
|
||||
if ($thumb !== '') {
|
||||
return $thumb;
|
||||
}
|
||||
|
||||
$videoId = parse_youtube_id(trim($contentUrl));
|
||||
if ($videoId !== '') {
|
||||
return 'https://img.youtube.com/vi/' . $videoId . '/sddefault.jpg';
|
||||
}
|
||||
|
||||
return '/img/video/img_thumb_01.png';
|
||||
}
|
||||
|
||||
function highlight_keyword(string $text, string $keyword): string
|
||||
{
|
||||
$escaped = h($text);
|
||||
$keyword = trim($keyword);
|
||||
if ($keyword === '') {
|
||||
return $escaped;
|
||||
}
|
||||
|
||||
$pattern = '/' . preg_quote(h($keyword), '/') . '/iu';
|
||||
return (string)preg_replace($pattern, '<span class="search-highlight">$0</span>', $escaped);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db_conn();
|
||||
|
||||
$memberId = trim((string)($_SESSION['member_id'] ?? ''));
|
||||
$sysCompCode = trim((string)($_SESSION['sys_comp_code'] ?? ''));
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
c.content_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.description2,
|
||||
c.content_url,
|
||||
c.thumbnail_url,
|
||||
c.category_code,
|
||||
c.category_group,
|
||||
COALESCE(ec.code_name, c.category_code) AS category_name,
|
||||
COALESCE(vs.view_cnt, 0) AS view_cnt,
|
||||
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked
|
||||
FROM edu_contents c
|
||||
LEFT JOIN edu_codes ec
|
||||
ON ec.base_code = c.category_code
|
||||
AND ec.group_code = 'CA100'
|
||||
AND ec.is_active = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
content_id,
|
||||
COUNT(DISTINCT member_id) AS view_cnt
|
||||
FROM edu_learning_histories
|
||||
GROUP BY content_id
|
||||
) vs
|
||||
ON vs.content_id = c.content_id
|
||||
LEFT JOIN edu_content_wishlist cw
|
||||
ON cw.member_id = :mid
|
||||
AND cw.sys_comp_code = :sc
|
||||
AND cw.is_active = 1
|
||||
AND cw.content_id = c.content_id
|
||||
WHERE c.is_active = 1
|
||||
AND (
|
||||
:kw = ''
|
||||
OR c.title LIKE :like_kw1
|
||||
OR c.description LIKE :like_kw2
|
||||
OR c.description2 LIKE :like_kw3
|
||||
)
|
||||
ORDER BY c.updated_at DESC, c.created_at DESC, c.content_id DESC
|
||||
LIMIT 300
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':mid' => $memberId,
|
||||
':sc' => $sysCompCode,
|
||||
':kw' => $q,
|
||||
':like_kw1' => '%' . $q . '%',
|
||||
':like_kw2' => '%' . $q . '%',
|
||||
':like_kw3' => '%' . $q . '%',
|
||||
]);
|
||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (Throwable $e) {
|
||||
error_log('[search_result] ' . $e->getMessage());
|
||||
$results = [];
|
||||
}
|
||||
|
||||
$totalCount = count($results);
|
||||
|
||||
// 카테고리별 그룹화
|
||||
$groupedResults = [];
|
||||
$categoryMeta = []; // category_code => ['name' => string, 'count' => int]
|
||||
foreach ($results as $row) {
|
||||
$catCode = (string)($row['category_code'] ?? 'etc');
|
||||
$catName = (string)($row['category_name'] ?? $row['category_code'] ?? '기타');
|
||||
if (!isset($groupedResults[$catCode])) {
|
||||
$groupedResults[$catCode] = [];
|
||||
$categoryMeta[$catCode] = ['name' => $catName, 'count' => 0];
|
||||
}
|
||||
$groupedResults[$catCode][] = $row;
|
||||
$categoryMeta[$catCode]['count']++;
|
||||
}
|
||||
|
||||
$videosForJs = array_map(static function (array $row): array {
|
||||
$contentUrl = (string)($row['content_url'] ?? '');
|
||||
$thumb = make_thumb_url((string)($row['thumbnail_url'] ?? ''), $contentUrl);
|
||||
|
||||
return [
|
||||
'id' => (string)($row['content_id'] ?? ''),
|
||||
'content_id' => (string)($row['content_id'] ?? ''),
|
||||
'url' => $contentUrl,
|
||||
'content_url' => $contentUrl,
|
||||
'thumbnail' => $thumb,
|
||||
'category' => (string)($row['category_name'] ?? $row['category_code'] ?? ''),
|
||||
'category_name' => (string)($row['category_name'] ?? $row['category_code'] ?? ''),
|
||||
'category_code' => (string)($row['category_code'] ?? ''),
|
||||
'subcate' => (string)($row['category_group'] ?? ''),
|
||||
'title' => (string)($row['title'] ?? ''),
|
||||
'description' => trim((string)($row['description'] ?? '')),
|
||||
'description2' => trim((string)($row['description2'] ?? '')),
|
||||
'bookmark' => ((string)($row['is_bookmarked'] ?? '0') === '1'),
|
||||
'type' => 'main',
|
||||
'keywords' => [],
|
||||
'watch_tm' => 0,
|
||||
'content_tm' => 0,
|
||||
'all_tm' => 0,
|
||||
'view_cnt' => (int)($row['view_cnt'] ?? 0),
|
||||
];
|
||||
}, $results);
|
||||
|
||||
$videosJson = json_encode($videosForJs, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP);
|
||||
?>
|
||||
<!doctype html>
|
||||
<!-- dynamic-search-result-v2 -->
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?php include(__DIR__ . '/_include/_head.php') ?>
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css" />
|
||||
<style>
|
||||
.search-result .search-highlight {
|
||||
color: #ff7a00;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-result .item-desc-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-result .item-view-count {
|
||||
margin-left: auto;
|
||||
font-size: 13px;
|
||||
color: #6f6f6f;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap search-result">
|
||||
<?php include(__DIR__ . '/_include/_header.php') ?>
|
||||
<div class="container">
|
||||
<div class="search-result-wrap">
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h3 class="blind">검색결과</h3>
|
||||
<p class="search-summary">
|
||||
'<?php echo h($q); ?>'에 대한 검색결과가 <em class="search-count"><?php echo (int)$totalCount; ?></em>건 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($q === ''): ?>
|
||||
<p style="padding: 20px 0;">검색어를 입력해주세요.</p>
|
||||
<?php elseif ($totalCount === 0): ?>
|
||||
<p style="padding: 20px 0;">검색 결과가 없습니다.</p>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- 필터 탭 -->
|
||||
<div class="filter-tabs">
|
||||
<button class="filter-tab active" data-filter="all">
|
||||
전체 <span class="count"><?php echo (int)$totalCount; ?></span>
|
||||
</button>
|
||||
<?php foreach ($categoryMeta as $catCode => $meta): ?>
|
||||
<button class="filter-tab" data-filter="<?php echo h($catCode); ?>">
|
||||
<?php echo h($meta['name']); ?> <span class="count"><?php echo (int)$meta['count']; ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- 검색 결과 섹션 -->
|
||||
<div class="search-sections" id="videoCardsContainer">
|
||||
|
||||
<!-- [전체] 평면 목록: 카테고리 구분 없이 전체 결과를 순서대로 출력 -->
|
||||
<section class="content-section" data-section="all">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">전체 <span class="section-count"><?php echo (int)$totalCount; ?></span></h3>
|
||||
<!-- <div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option>조회수</option>
|
||||
<option selected>업데이트</option>
|
||||
<option>내가본컨텐츠</option>
|
||||
<option>안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<ul class="video-grid">
|
||||
<?php foreach ($results as $idx => $row):
|
||||
$id = (string)($row['content_id'] ?? '');
|
||||
$title = (string)($row['title'] ?? '');
|
||||
$description = trim((string)($row['description'] ?? ''));
|
||||
$description2 = trim((string)($row['description2'] ?? ''));
|
||||
$thumb = make_thumb_url((string)($row['thumbnail_url'] ?? ''), (string)($row['content_url'] ?? ''));
|
||||
$category = (string)($row['category_name'] ?? $row['category_code'] ?? '');
|
||||
$bookmark = ((string)($row['is_bookmarked'] ?? '0') === '1');
|
||||
$viewCnt = (int)($row['view_cnt'] ?? 0);
|
||||
$bookmarkId = 'like_chk_all_' . ($idx + 1);
|
||||
$titleHighlighted = highlight_keyword($title, $q);
|
||||
$descHighlighted = highlight_keyword($description, $q);
|
||||
$desc2Highlighted = highlight_keyword($description2, $q);
|
||||
?>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link card" data-video-id="<?php echo h($id); ?>">
|
||||
<label class="bookmark" for="<?php echo h($bookmarkId); ?>" onclick="event.stopPropagation();"><input type="checkbox" id="<?php echo h($bookmarkId); ?>" <?php echo $bookmark ? 'checked' : ''; ?> title="좋아요"></label>
|
||||
<div class="item-thumb">
|
||||
<img src="<?php echo h($thumb); ?>" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title"><?php echo $titleHighlighted; ?></strong>
|
||||
<?php if ($description !== '' || $description2 !== ''): ?>
|
||||
<div class="item-desc" style="color:#6f6f6f; font-size:13px; line-height:1.5;">
|
||||
<?php if ($description !== ''): ?><p class="desc-1"><?php echo $descHighlighted; ?></p><?php endif; ?>
|
||||
<?php if ($description2 !== ''): ?><p class="desc-2" style="margin-top:2px;"><?php echo $desc2Highlighted; ?></p><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="tag-list item-desc-wrapper">
|
||||
<span class="tag"><?php echo h($category); ?></span>
|
||||
<span class="item-view-count">시청수 <?php echo (int)$viewCnt; ?> 회</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- [카테고리별] 섹션: 기본 hidden, 특정 탭 선택 시 표시 -->
|
||||
<?php
|
||||
$globalIdx = 0;
|
||||
foreach ($groupedResults as $catCode => $catRows):
|
||||
$catName = $categoryMeta[$catCode]['name'];
|
||||
$catCount = $categoryMeta[$catCode]['count'];
|
||||
?>
|
||||
<section class="content-section" data-section="<?php echo h($catCode); ?>" style="display:none;">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title"><?php echo h($catName); ?> <span class="section-count"><?php echo (int)$catCount; ?></span></h3>
|
||||
<!-- <div class="list-options">
|
||||
<div class="select-wrap">
|
||||
<select class="select-sort" title="정렬">
|
||||
<option>조회수</option>
|
||||
<option selected>업데이트</option>
|
||||
<option>내가본컨텐츠</option>
|
||||
<option>안본컨텐츠</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<ul class="video-grid">
|
||||
<?php foreach ($catRows as $row):
|
||||
$globalIdx++;
|
||||
$id = (string)($row['content_id'] ?? '');
|
||||
$title = (string)($row['title'] ?? '');
|
||||
$description = trim((string)($row['description'] ?? ''));
|
||||
$description2 = trim((string)($row['description2'] ?? ''));
|
||||
$thumb = make_thumb_url((string)($row['thumbnail_url'] ?? ''), (string)($row['content_url'] ?? ''));
|
||||
$category = (string)($row['category_name'] ?? $row['category_code'] ?? '');
|
||||
$bookmark = ((string)($row['is_bookmarked'] ?? '0') === '1');
|
||||
$viewCnt = (int)($row['view_cnt'] ?? 0);
|
||||
$bookmarkId = 'like_chk_search_' . $globalIdx;
|
||||
$titleHighlighted = highlight_keyword($title, $q);
|
||||
$descHighlighted = highlight_keyword($description, $q);
|
||||
$desc2Highlighted = highlight_keyword($description2, $q);
|
||||
?>
|
||||
<li class="video-item">
|
||||
<a href="#" class="card-link card" data-video-id="<?php echo h($id); ?>">
|
||||
<label class="bookmark" for="<?php echo h($bookmarkId); ?>" onclick="event.stopPropagation();"><input type="checkbox" id="<?php echo h($bookmarkId); ?>" <?php echo $bookmark ? 'checked' : ''; ?> title="좋아요"></label>
|
||||
<div class="item-thumb">
|
||||
<img src="<?php echo h($thumb); ?>" alt="" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<strong class="item-title"><?php echo $titleHighlighted; ?></strong>
|
||||
<?php if ($description !== '' || $description2 !== ''): ?>
|
||||
<div class="item-desc" style="color:#6f6f6f; font-size:13px; line-height:1.5;">
|
||||
<?php if ($description !== ''): ?><p class="desc-1"><?php echo $descHighlighted; ?></p><?php endif; ?>
|
||||
<?php if ($description2 !== ''): ?><p class="desc-2" style="margin-top:2px;"><?php echo $desc2Highlighted; ?></p><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="tag-list item-desc-wrapper">
|
||||
<span class="tag"><?php echo h($category); ?></span>
|
||||
<span class="item-view-count">시청수 <?php echo (int)$viewCnt; ?> 회</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/learning/config.js" defer></script>
|
||||
<script src="/js/learning/markers.js" defer></script>
|
||||
<script src="/js/learning/modal.js" defer></script>
|
||||
<script src="/js/bridges/learning-modal-bridge.js" defer></script>
|
||||
<script>
|
||||
window.puzzleConfig = {
|
||||
...(window.puzzleConfig || {}),
|
||||
COMPLETION_MODE: 'COMPLETED',
|
||||
CURRENT_MEMBER_ID: <?php echo json_encode($memberId, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP); ?>,
|
||||
};
|
||||
</script>
|
||||
<script src="/js/puzzle-onboarding.js" defer></script>
|
||||
<script src="/js/bridges/onboarding-modal-bridge.js" defer></script>
|
||||
<script src="/js/main/Videomodalmanager.js" defer></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const currentVideos = <?= $videosJson ?: '[]' ?>;
|
||||
const container = document.getElementById('videoCardsContainer');
|
||||
|
||||
// ── 모달 초기화 ──
|
||||
let modalManager = null;
|
||||
if (container && Array.isArray(currentVideos) && currentVideos.length > 0 && typeof VideoModalManager !== 'undefined') {
|
||||
modalManager = new VideoModalManager({ videos: currentVideos });
|
||||
}
|
||||
|
||||
// ── 필터 탭 ──
|
||||
const filterTabs = document.querySelectorAll('.filter-tab');
|
||||
filterTabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
filterTabs.forEach(function (t) { t.classList.remove('active'); });
|
||||
this.classList.add('active');
|
||||
|
||||
const filter = this.dataset.filter;
|
||||
document.querySelectorAll('.content-section').forEach(function (section) {
|
||||
const sec = section.dataset.section;
|
||||
if (filter === 'all') {
|
||||
// 전체 탭: all 섹션만 표시, 카테고리 섹션 모두 숨김
|
||||
section.style.display = (sec === 'all') ? 'block' : 'none';
|
||||
} else {
|
||||
// 카테고리 탭: all 섹션 숨기고 해당 카테고리만 표시
|
||||
section.style.display = (sec === filter) ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── 북마크 ──
|
||||
if (!container) return;
|
||||
|
||||
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, isBookmarked) {
|
||||
const id = String(videoId || '');
|
||||
currentVideos.forEach(function (video) {
|
||||
if (String(video?.id ?? '') === id) { video.bookmark = !!isBookmarked; }
|
||||
});
|
||||
container.querySelectorAll('.card[data-video-id]').forEach(function (cardEl) {
|
||||
if (String(cardEl.getAttribute('data-video-id') || '') !== id) return;
|
||||
const checkbox = cardEl.querySelector('.bookmark input[type="checkbox"]');
|
||||
if (checkbox) checkbox.checked = !!isBookmarked;
|
||||
});
|
||||
if (modalManager && modalManager.config) { modalManager.config.videos = currentVideos; }
|
||||
}
|
||||
|
||||
function getVideoDataById(videoId) {
|
||||
const id = String(videoId || '');
|
||||
return currentVideos.find(function (v) {
|
||||
return String(v?.id ?? '') === id || String(v?.content_id ?? '') === id;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function toBridgeInfo(videoData) {
|
||||
const contentId = String(videoData?.content_id ?? videoData?.id ?? '');
|
||||
const description = [
|
||||
String(videoData?.description || '').trim(),
|
||||
String(videoData?.description2 || '').trim(),
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
return {
|
||||
contentId: contentId,
|
||||
videoUrl: String(videoData?.content_url ?? videoData?.url ?? ''),
|
||||
title: String(videoData?.title ?? ''),
|
||||
description: description,
|
||||
categoryName: String(videoData?.category_name ?? videoData?.category ?? ''),
|
||||
subCategory: String(videoData?.subcate ?? ''),
|
||||
watchTm: Number(videoData?.watch_tm || 0),
|
||||
contentTm: Number(videoData?.content_tm || 0),
|
||||
bookmark: !!videoData?.bookmark,
|
||||
};
|
||||
}
|
||||
|
||||
async function openSearchResultVideoByCategory(videoData) {
|
||||
const categoryCode = String(videoData?.category_code || '').toUpperCase();
|
||||
const bridgeInfo = toBridgeInfo(videoData);
|
||||
|
||||
// 브리지가 준비된 카테고리부터 우선 적용한다.
|
||||
if (categoryCode === 'CA10006' && typeof window._biztrendOpenModal === 'function') {
|
||||
window._biztrendOpenModal(bridgeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryCode === 'CA10005' && typeof window._insightOpenModal === 'function') {
|
||||
window._insightOpenModal(bridgeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryCode === 'CA10004' && typeof window._leadershipOpenModal === 'function') {
|
||||
window._leadershipOpenModal(bridgeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryCode === 'CA10003' && typeof window._learningOpenModal === 'function') {
|
||||
window._learningOpenModal(bridgeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryCode === 'CA10002' && typeof window._onboardingOpenModal === 'function') {
|
||||
window._onboardingOpenModal(bridgeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 기본값: 기존 공통 모달 흐름 유지
|
||||
if (modalManager && typeof modalManager.openVideo === 'function') {
|
||||
await modalManager.openVideo(String(videoData?.id ?? videoData?.content_id ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.bookmark')) { e.stopPropagation(); return; }
|
||||
|
||||
const card = e.target.closest('.card[data-video-id]');
|
||||
if (!card) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const videoId = String(card.getAttribute('data-video-id') || '');
|
||||
if (!videoId) return;
|
||||
|
||||
const videoData = getVideoDataById(videoId);
|
||||
if (!videoData) return;
|
||||
|
||||
openSearchResultVideoByCategory(videoData).catch(function (err) {
|
||||
console.warn('[search_result] open by category failed:', err?.message || err);
|
||||
});
|
||||
});
|
||||
|
||||
container.addEventListener('change', async function (e) {
|
||||
const checkbox = e.target.closest('input[type="checkbox"]');
|
||||
if (!checkbox || !checkbox.closest('.bookmark')) 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; return; }
|
||||
setBookmarkState(videoId, nextState);
|
||||
} catch (err) {
|
||||
checkbox.checked = !nextState;
|
||||
console.warn('[search wishlist]', err?.message || err);
|
||||
} finally {
|
||||
checkbox.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user