Files
edu/skin/leadership.php

1500 lines
52 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../bbs/auth.php';
edu_require_login();
?>
<!doctype html>
<html lang="ko">
<head>
<?php include(__DIR__ . "/_include/_head.php") ?>
</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">
<div class="container-inner">
<!-- 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 if (!empty($array_leadership_banner)): ?>
<?php foreach ($array_leadership_banner as $banner): ?>
<div class="swiper-slide">
<div class="banner-title">
<picture>
<source media="(min-width: 1025px)" srcset="<?= htmlspecialchars($banner['banner_title_pc'], ENT_QUOTES, 'UTF-8') ?>">
<source media="(max-width: 1024px)" srcset="<?= htmlspecialchars($banner['banner_title_mo'], ENT_QUOTES, 'UTF-8') ?>">
<img src="<?= htmlspecialchars($banner['banner_title_pc'], ENT_QUOTES, 'UTF-8') ?>" alt="leadership">
</picture>
</div>
<div class="banner-grid">
<div class="banner-left">
<dl>
<dt>
<?= htmlspecialchars($banner['description1'], ENT_QUOTES, 'UTF-8') ?><br>
<?= htmlspecialchars($banner['description2'], ENT_QUOTES, 'UTF-8') ?>
</dt>
<dd><?= nl2br(htmlspecialchars($banner['description3'], ENT_QUOTES, 'UTF-8')) ?></dd>
</dl>
</div>
<div class="banner-right">
<div class="banner-img">
<img src="<?= htmlspecialchars($banner['thumbnail'], ENT_QUOTES, 'UTF-8') ?>" alt="<?= htmlspecialchars($banner['title'], ENT_QUOTES, 'UTF-8') ?>">
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<?php foreach ($array_banner_img as $i => $bn): ?>
<div class="swiper-slide">
<div class="banner-img">
<picture>
<source media="(min-width: 1025px)" srcset="<?= $bn['normal'] ?>">
<source media="(max-width: 1024px)" srcset="<?= $bn['mobile'] ?>">
<img src="<?= $bn['normal'] ?>" alt="<?= $bn['title'] ?>">
</picture>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</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">전체 <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>
</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 () {
$('.container-inner').on('scroll', function () {
console.log(".container scroll");
if (loading || lastPage) return;
const scrollTop = this.scrollTop;
const windowHeight = this.clientHeight;
const docHeight = this.scrollHeight;
console.log("scrollTop="+scrollTop);
console.log("windowHeight="+windowHeight);
console.log("docHeight="+docHeight);
console.log("scrollTop + windowHeight=");
console.log((scrollTop + windowHeight));
console.log("docHeight - 100=");
console.log((docHeight - 100));
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 normalizeBookmarkMarkup() {
// 서버 렌더 템플릿의 인라인 onclick(event.stopPropagation()) 때문에
// 위임 이벤트가 막히는 경우가 있어 제거한다.
$('#video-list .bookmark').removeAttr('onclick');
}
function applyKeywordDesc() {
$('#video-list .video-item').each(function () {
var $item = $(this);
var rawKeywords = String(
$item.attr('data-keywords')
|| $item.data('keywords')
|| $item.attr('data-keyword')
|| $item.data('keyword')
|| ''
).trim();
if (!rawKeywords) return;
var keywordText = rawKeywords
.split(/[\,\|]/)
.map(function (kw) { return String(kw || '').trim(); })
.filter(Boolean)
.map(function (kw) { return kw.indexOf('#') === 0 ? kw : ('#' + kw); })
.join(' ');
if (!keywordText) return;
var $desc = $item.find('.item-desc').first();
if ($desc.length) {
$desc.text(keywordText);
}
});
}
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);
normalizeBookmarkMarkup();
applyKeywordDesc();
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);
normalizeBookmarkMarkup();
applyKeywordDesc();
}
}
},
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; } */
/* 영상 리스트 키워드 + 조회수 */
.item-desc-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px;
}
.item-desc {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-view-count {
margin-top: 10px;
flex-shrink: 0;
white-space: nowrap;
font-size: 14px;
color: rgba(7, 7, 7, 0.6);
}
/* 댓글 영역 고정 레이아웃을 유지하기 위해 추천 목록 ul에만 스크롤 적용 */
#leadershipVideoModal .video-side .video-list {
overflow: hidden;
}
#leadershipVideoModal #recommendedList {
max-height: none;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
padding-right: 4px;
}
#leadershipVideoModal #recommendedList::-webkit-scrollbar {
width: 6px;
}
#leadershipVideoModal #recommendedList::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.35);
border-radius: 999px;
}
#leadershipVideoModal #recommendedList::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.08);
}
#leadershipVideoModal .comment-wrap {
display: flex;
flex-direction: column;
min-height: 220px;
box-sizing: border-box;
}
#leadershipVideoModal .comment-resizer {
touch-action: none;
user-select: none;
}
#leadershipVideoModal .comment-list-wrap {
flex: 1;
min-height: 0;
overflow-y: auto;
}
#leadershipVideoModal .comment-box {
flex-shrink: 0;
}
/* 1024px 이하: 댓글 패널(슬라이드업) 토글 */
@media (max-width: 1024px) {
#leadershipVideoModal .comment-wrap {
height: min(48vh, 420px);
}
#leadershipVideoModal .comment-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
width: 100%;
max-height: min(70vh, 520px);
z-index: 1002;
transform: translateY(100%);
transition: transform 220ms ease;
}
#leadershipVideoModal.is-comment-open .comment-wrap {
transform: translateY(0);
}
}
</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>
<div class="tit-right">
<button class="btn-comment"><i class="ico-comment"></i>0</button>
<label class="bookmark" for="leadership-bookmark-chk">
<input id="leadership-bookmark-chk" type="checkbox" value="" />
<span>북마크</span>
</label>
</div>
</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-resizer">
<div class="resizer-handle"></div>
</div>
<!-- 댓글 목록 -->
<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;
var commentToggleCleanup = 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;
// 30초마다 서버에 heartbeat 전송
if (lastSaveTime >= 30) {
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) {
if ($(e.target).closest('.bookmark').length) {
e.preventDefault();
e.stopPropagation();
return;
}
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
});
});
// ── 리스트 카드 북마크 토글/저장 ──
// input 클릭은 브라우저 기본 토글을 사용하고, label 영역 클릭만 수동 토글한다.
function saveListBookmark($chk, contentId, isActive) {
$chk.prop('disabled', true);
$.ajax({
url: '/bbs/api/save_wishlist.php',
type: 'POST',
data: { content_id: contentId, is_active: isActive },
dataType: 'json',
success: function (res) {
if (!res || res.success !== true) {
$chk.prop('checked', isActive !== '1');
return;
}
// 같은 영상을 모달에서 보고 있으면 모달 북마크도 동기화
if (currentContentId && String(currentContentId) === String(contentId)) {
$modal.find('.bookmark input[type="checkbox"]').prop('checked', isActive === '1');
}
},
error: function () {
$chk.prop('checked', isActive !== '1');
},
complete: function () {
$chk.prop('disabled', false);
}
});
}
$(document).on('click', '#video-list .bookmark', function (e) {
if ($(e.target).is('input[type="checkbox"]')) {
// input은 기본 토글을 허용하고 change에서 저장
e.stopPropagation();
return;
}
e.preventDefault();
e.stopPropagation();
var $item = $(this).closest('.video-item');
var contentId = String($item.data('content-id') || $item.attr('data-content-id') || '');
if (!contentId) return;
var $chk = $(this).find('input[type="checkbox"]');
if (!$chk.length || $chk.prop('disabled')) return;
$chk.prop('checked', !$chk.prop('checked')).trigger('change');
});
$(document).on('change', '#video-list .bookmark input[type="checkbox"]', function (e) {
e.stopPropagation();
var $chk = $(this);
var $item = $chk.closest('.video-item');
var contentId = String($item.data('content-id') || $item.attr('data-content-id') || '');
if (!contentId) return;
var isActive = $chk.is(':checked') ? '1' : '0';
saveListBookmark($chk, contentId, isActive);
});
// ── 모달 열기 ──
function adjustRecommendedListHeight() {
if (!$modal.is(':visible')) return;
var $videoListWrap = $modal.find('.video-list');
var $list = $modal.find('#recommendedList');
var $commentWrap = $modal.find('.comment-wrap');
if (!$videoListWrap.length || !$list.length || !$commentWrap.length) return;
var listTop = $videoListWrap.offset().top;
var commentTop = $commentWrap.offset().top;
if (!Number.isFinite(listTop) || !Number.isFinite(commentTop)) return;
var availableHeight = Math.floor(commentTop - listTop - 8);
if (availableHeight < 120) availableHeight = 120;
$list.css('max-height', availableHeight + 'px');
}
// ── 리더십 댓글 패널 토글 + 드래그 리사이즈 ──
function setupCommentToggle() {
// 중복 바인딩 방지
if (typeof commentToggleCleanup === 'function') return;
var $btnComment = $modal.find('.btn-comment').first();
var $videoSide = $modal.find('.video-side').first();
var $commentWrap = $modal.find('.comment-wrap').first();
if (!$btnComment.length || !$videoSide.length || !$commentWrap.length) return;
var mediaQuery = window.matchMedia('(max-width: 1024px)');
var dragState = null;
function getViewportHeight() {
if (window.visualViewport && window.visualViewport.height) {
return window.visualViewport.height;
}
return window.innerHeight || document.documentElement.clientHeight || 0;
}
function getMinCommentHeight() {
return 220;
}
function getMaxCommentHeight() {
if (!isMobileView()) {
var videoSideHeight = $videoSide.innerHeight() || 0;
var videoHeaderHeight = $videoSide.find('.video-header').outerHeight(true) || 0;
if (videoSideHeight > videoHeaderHeight) {
return Math.max(getMinCommentHeight(), videoSideHeight - videoHeaderHeight - 40);
}
}
return Math.min(Math.floor(getViewportHeight() * 0.7), 520);
}
function clampCommentHeight(height) {
var minHeight = getMinCommentHeight();
var maxHeight = getMaxCommentHeight();
if (maxHeight < minHeight) {
minHeight = Math.max(160, maxHeight);
}
return Math.max(minHeight, Math.min(height, maxHeight));
}
function getDefaultCommentHeight() {
return clampCommentHeight(Math.floor(getViewportHeight() * 0.48));
}
function getPointerY(event) {
var originalEvent = event && event.originalEvent ? event.originalEvent : event;
if (originalEvent && originalEvent.touches && originalEvent.touches.length) {
return originalEvent.touches[0].clientY;
}
if (originalEvent && originalEvent.changedTouches && originalEvent.changedTouches.length) {
return originalEvent.changedTouches[0].clientY;
}
return originalEvent ? originalEvent.clientY : NaN;
}
function applyCommentHeight(height) {
var nextHeight = clampCommentHeight(Math.round(height));
$commentWrap.css('height', nextHeight + 'px');
adjustRecommendedListHeight();
}
function isMobileView() {
return mediaQuery.matches;
}
function openComment() {
applyCommentHeight(parseFloat($commentWrap[0].style.height) || getDefaultCommentHeight());
$modal.addClass('is-comment-open');
}
function closeComment() {
$modal.removeClass('is-comment-open');
}
function clearMobileStyles() {
var el = $commentWrap.get(0);
if (el && el.style) el.style.cssText = '';
var listWrap = $commentWrap.find('.comment-list-wrap').get(0);
if (listWrap && listWrap.style) listWrap.style.cssText = '';
}
function restoreToVideoSide() {
closeComment();
if ($commentWrap.parent().is($modal)) {
$videoSide.append($commentWrap);
}
clearMobileStyles();
}
function moveToModal() {
if (!$commentWrap.parent().is($modal)) {
$modal.append($commentWrap);
}
}
function handleResize() {
if (isMobileView()) {
moveToModal();
if ($modal.hasClass('is-comment-open')) {
applyCommentHeight(parseFloat($commentWrap[0].style.height) || getDefaultCommentHeight());
}
} else {
restoreToVideoSide();
}
}
function onResizerPointerMove(event) {
if (!dragState) return;
var pointerY = getPointerY(event);
if (!Number.isFinite(pointerY)) return;
var delta = dragState.startY - pointerY;
if (Math.abs(delta) > 2) {
dragState.moved = true;
}
applyCommentHeight(dragState.startHeight + delta);
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
}
function stopResizerDrag() {
dragState = null;
$(document).off('.leadershipCommentResize');
}
function startResizerDrag(event) {
if (event.type === 'mousedown' && event.which !== 1) return;
var pointerY = getPointerY(event);
if (!Number.isFinite(pointerY)) return;
if (isMobileView()) {
openComment();
}
dragState = {
startY: pointerY,
startHeight: $commentWrap.outerHeight() || getDefaultCommentHeight(),
moved: false
};
$(document)
.on('mousemove.leadershipCommentResize touchmove.leadershipCommentResize', onResizerPointerMove)
.on('mouseup.leadershipCommentResize touchend.leadershipCommentResize touchcancel.leadershipCommentResize', stopResizerDrag);
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
}
// 초기 상태 반영
if (isMobileView()) {
moveToModal();
} else {
restoreToVideoSide();
}
// matchMedia change 감지
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', handleResize);
} else if (typeof mediaQuery.addListener === 'function') {
mediaQuery.addListener(handleResize);
}
// 버튼 클릭 토글
var onBtnClick = function (e) {
e.stopPropagation();
if (!isMobileView()) return;
if ($modal.hasClass('is-comment-open')) closeComment();
else openComment();
};
$btnComment.on('click', onBtnClick);
$commentWrap.on('mousedown touchstart', '.comment-resizer', startResizerDrag);
// 정리 함수 등록
commentToggleCleanup = function () {
try {
$btnComment.off('click', onBtnClick);
$commentWrap.off('mousedown touchstart', '.comment-resizer', startResizerDrag);
stopResizerDrag();
if (typeof mediaQuery.removeEventListener === 'function') {
mediaQuery.removeEventListener('change', handleResize);
} else if (typeof mediaQuery.removeListener === 'function') {
mediaQuery.removeListener(handleResize);
}
} catch (e) {}
restoreToVideoSide();
commentToggleCleanup = null;
};
}
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');
// 모바일 댓글 토글 세팅(1회 바인딩)
setupCommentToggle();
// 추천영역을 댓글 시작점까지 확장
setTimeout(adjustRecommendedListHeight, 0);
}
// ── 모달 닫기 (진행상황 저장 후 닫기) ──
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();
$modal.removeClass('is-comment-open');
$('body').css('overflow', '');
currentContentId = null;
editingCommentId = null;
// 댓글 토글 리스너/DOM 정리
if (typeof commentToggleCleanup === 'function') {
commentToggleCleanup();
}
}
$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;') + '" ' +
'data-sub-category="' + (v.subcate || '').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);
});
setTimeout(adjustRecommendedListHeight, 0);
})
.fail(function () {
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
setTimeout(adjustRecommendedListHeight, 0);
});
}
$(window).on('resize', function () {
if ($modal.is(':visible')) {
adjustRecommendedListHeight();
}
});
// ── 추천영상 클릭 → 영상 전환 ──
$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;
var recCid = $a.attr('data-content-id') || $a.data('content-id') || '';
var recSubCat = $a.attr('data-sub-category') || '';
// DOM fallback: 같은 content_id가 #video-list에 있으면 거기서 sub-category 보완
if (!recSubCat && recCid) {
var $card = $('#video-list .video-item[data-content-id="' + recCid + '"]');
if ($card.length) {
recSubCat = $card.attr('data-sub-category') || '';
}
}
openModal({
contentId: recCid,
videoUrl: $a.attr('data-content-url') || $a.data('content-url') || '',
title: $a.data('title') || $a.find('.title').text().trim(),
description: $a.data('description') || '',
categoryName: $a.data('category-name') || '리더십',
subCategory: recSubCat
});
});
// ══════════════════════════════════════════════
// 댓글 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="' + (c.profile_image || '/img/ico/ico_user.svg') + '" onerror="this.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.setProperty('overflow-y', 'hidden', 'important');
this.style.setProperty('overflow-x', 'hidden', 'important');
this.style.setProperty('max-height', 'none', 'important');
this.style.setProperty('height', '0px', 'important');
var h = this.scrollHeight;
this.style.setProperty('height', (h > 0 ? h : 40) + 'px', 'important');
});
}
// ── 댓글 인라인 수정 (온보딩 동일) ──
$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;
if (newComment.length > COMMENT_MAX_LENGTH) {
alert('댓글은 200자까지 입력할 수 있습니다.');
$textarea.val($textarea.val().slice(0, COMMENT_MAX_LENGTH));
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;
if (comment.length > COMMENT_MAX_LENGTH) {
$textarea.val($textarea.val().slice(0, COMMENT_MAX_LENGTH));
alert('댓글은 200자까지 입력할 수 있습니다.');
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();
});
var COMMENT_MAX_LENGTH = 200;
var _lastLengthAlertAt = 0;
// ── textarea 입력 → 버튼 활성화 + 200자 제한 ──
$modal.on('input', '.comment-box textarea', function () {
var $ta = $(this);
if ($ta.val().length > COMMENT_MAX_LENGTH) {
$ta.val($ta.val().slice(0, COMMENT_MAX_LENGTH));
var now = Date.now();
if (now - _lastLengthAlertAt > 800) {
_lastLengthAlertAt = now;
alert('댓글은 200자까지 입력할 수 있습니다.');
}
}
var hasText = $ta.val().trim().length > 0;
$modal.find('.btn-cancel').prop('disabled', !hasText);
$modal.find('.btn-save').prop('disabled', !hasText);
});
function resetCommentForm() {
var $ta = $modal.find('.comment-box textarea');
$ta.val('');
$ta.attr('maxlength', COMMENT_MAX_LENGTH);
$modal.find('.btn-cancel').prop('disabled', true);
$modal.find('.btn-save').prop('disabled', true).text('등록');
editingCommentId = null;
}
});
</script>
</body>
</html>