Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+532
View File
@@ -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.
+413
View File
@@ -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>
+106
View File
@@ -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 -->
+152
View File
@@ -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>
+39
View File
@@ -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;
}
}
+52
View File
@@ -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>
+589
View File
@@ -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>
+153
View File
@@ -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>
+139
View File
@@ -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>
+132
View File
@@ -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>
<!-- // 목표 상세 팝업 레이어 -->
+50
View File
@@ -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">키워드는&nbsp;<strong>최대 3개</strong>까지 선택 가능합니다.</span>
</p>
</div>
</div>
<!-- // keyword modal -->
+184
View File
@@ -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>
+50
View File
@@ -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">&times;</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>
+14
View File
@@ -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'],
];
}
}
+2
View File
@@ -0,0 +1,2 @@
<?php
require __DIR__ . '/video.php';
+169
View File
@@ -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">&times;</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>
+169
View File
@@ -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">&times;</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>
+76
View File
@@ -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">&times;</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>
+70
View File
@@ -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);
}
+88
View File
@@ -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>
+271
View File
@@ -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>
+388
View File
@@ -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>
+334
View File
@@ -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>
+278
View File
@@ -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
+571
View File
@@ -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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&#8249;</span>
<span class="mobile-tab-label">시청완료</span>
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">&#8250;</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="닫기">&#10005;</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>
+542
View File
@@ -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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&#8249;</span>
<span class="mobile-tab-label">시청완료</span>
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">&#8250;</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="닫기">&#10005;</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>
+536
View File
@@ -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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&#8249;</span>
<span class="mobile-tab-label">시청완료</span>
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">&#8250;</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="닫기">&#10005;</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
View File
File diff suppressed because it is too large Load Diff
+119
View File
@@ -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
View File
@@ -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>
+532
View File
@@ -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>
+439
View File
@@ -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>
+421
View File
@@ -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>
+413
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+271
View File
@@ -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>
+255
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+964
View File
@@ -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">&times;</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, '&quot;') + '" ' +
'data-title="' + (v.title || '').replace(/"/g, '&quot;') + '" ' +
'data-description="' + (v.description || '').replace(/"/g, '&quot;') + '" ' +
'data-category-name="' + (v.category_name || '').replace(/"/g, '&quot;') + '">' +
'<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
var safeName = (c.member_name || '익명')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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>
+388
View File
@@ -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>
+334
View File
@@ -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>
+278
View File
@@ -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
+111
View File
@@ -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>
+85
View File
@@ -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>
+327
View File
@@ -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
View File
@@ -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>
+16
View File
@@ -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
View File
@@ -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>
+860
View File
@@ -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
View File
@@ -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="닫기">
&#10005;
</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&middot;</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">&#8249;</span>
<span class="mobile-tab-label">시청완료</span>
<span class="mobile-tab-arrow mobile-tab-next" aria-label="다음" aria-hidden="true">&#8250;</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="닫기">&#10005;</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>
+1128
View File
File diff suppressed because it is too large Load Diff
+337
View File
@@ -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>
+264
View File
@@ -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>
+266
View File
@@ -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>
+533
View File
@@ -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>