1538 lines
55 KiB
PHP
1538 lines
55 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../bbs/auth.php';
|
||
edu_require_login();
|
||
?>
|
||
<!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=인사이트
|
||
$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="./insight.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_insight_banner)): ?>
|
||
<?php foreach ($array_insight_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="<?= htmlspecialchars($banner['banner_title_alt'], ENT_QUOTES, 'UTF-8') ?>">
|
||
</picture>
|
||
</div>
|
||
<div class="banner-grid">
|
||
<div class="banner-left">
|
||
<?php if (!empty($banner['is_top_learner'])): ?>
|
||
<div class="profile-box">
|
||
<?php if (!empty($banner['offer_member_name'])): ?>
|
||
<div class="profile-img">
|
||
<span class="profile-crown">
|
||
<img src="/img/insight/crown.svg" alt="">
|
||
</span>
|
||
<span class="img-box" style="display:block; overflow:hidden; border-radius:50%;">
|
||
<img src="<?= htmlspecialchars($banner['offer_profile_image'], ENT_QUOTES, 'UTF-8') ?>" alt="<?= htmlspecialchars($banner['offer_member_name'], ENT_QUOTES, 'UTF-8') ?>" onerror="this.src='/img/ico/ico_user.svg'" style="display:block; width:100%; height:100%; object-fit:cover; border-radius:50%;">
|
||
</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="profile-info">
|
||
<p class="profile-name">
|
||
<strong class="name"><?= htmlspecialchars(trim($banner['offer_member_name'] . ' ' . $banner['offer_rank_name']), ENT_QUOTES, 'UTF-8') ?></strong>
|
||
<span class="department"><?= htmlspecialchars($banner['offer_dept_name'], ENT_QUOTES, 'UTF-8') ?></span>
|
||
</p>
|
||
<span class="label"><i class="ico-medal"></i>우수학습자</span>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
<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">
|
||
<a href="#"
|
||
class="banner-video-trigger"
|
||
data-content-id="<?= htmlspecialchars($banner['content_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||
data-content-url="<?= htmlspecialchars($banner['content_url'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||
data-title="<?= htmlspecialchars($banner['title'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||
data-description="<?= htmlspecialchars($banner['description3'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||
data-category-name="<?= htmlspecialchars($banner['category_name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||
data-sub-category="<?= htmlspecialchars($banner['sub_category_name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>"
|
||
data-watch-tm="0"
|
||
data-content-tm="0">
|
||
<img src="<?= htmlspecialchars($banner['thumbnail'], ENT_QUOTES, 'UTF-8') ?>" alt="<?= htmlspecialchars($banner['title'], ENT_QUOTES, 'UTF-8') ?>">
|
||
</a>
|
||
</div>
|
||
<?php if (!empty($banner['is_top_learner'])): ?>
|
||
<div class="profile-info">
|
||
<p class="profile-name">
|
||
<strong class="name"><?= htmlspecialchars(trim($banner['offer_member_name'] . ' ' . $banner['offer_rank_name']), ENT_QUOTES, 'UTF-8') ?></strong>
|
||
<span class="department"><?= htmlspecialchars($banner['offer_dept_name'], ENT_QUOTES, 'UTF-8') ?></span>
|
||
</p>
|
||
<span class="label"><i class="ico-medal"></i>우수학습자</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</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>
|
||
|
||
<!-- // 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 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;
|
||
|
||
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 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,
|
||
limit: 'all'
|
||
},
|
||
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>
|
||
/* #insightVideoModal .comment-list { list-style: none; margin: 0; padding: 0; }
|
||
#insightVideoModal .comment-list li { padding: 0; border-bottom: none; }
|
||
#insightVideoModal .comment-list li.empty-comment { padding: 20px; text-align: center; color: rgba(255,255,255,0.4); font-size: 13px; }
|
||
#insightVideoModal .comment-list .comment-info { display: flex; align-items: flex-start; }
|
||
#insightVideoModal .comment-list .comment-info .photo { flex-shrink: 0; margin-right: 6px; display: flex; align-items: center; }
|
||
#insightVideoModal .comment-list .comment-info .photo img { width: 20px; height: 20px; border-radius: 50%; }
|
||
#insightVideoModal .comment-list .comment-info .user-comment { flex: 1; min-width: 0; display: flex; align-items: center; }
|
||
#insightVideoModal .comment-list .comment-info .user-comment .user-name { font-size: 13px; color: #ccc; white-space: nowrap; flex-shrink: 0; margin-right: 8px; }
|
||
#insightVideoModal .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; }
|
||
#insightVideoModal .comment-list .comment-info .user-comment .user-text:disabled { opacity: 1; cursor: default; padding-top: 10px;}
|
||
#insightVideoModal .comment-list .comment-info .user-comment .user-text:not(:disabled) { border: 1px solid #555; border-radius: 4px; padding: 4px 8px; background: #2a2a2a; }
|
||
#insightVideoModal .comment-list .comment-info .actions { display: flex; gap: 4px; align-items: center; flex-shrink: 0; margin-left: 8px; align-self: center; }
|
||
#insightVideoModal .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; }
|
||
#insightVideoModal .comment-list .comment-info .actions button:hover { background: #555; color: #fff; }
|
||
#insightVideoModal .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);
|
||
}
|
||
|
||
/* 관련영상 목록 스크롤 */
|
||
#insightVideoModal .video-side .video-list {
|
||
overflow: hidden;
|
||
}
|
||
|
||
#insightVideoModal #recommendedList {
|
||
max-height: none;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
overscroll-behavior: contain;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
#insightVideoModal #recommendedList::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
#insightVideoModal #recommendedList::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.35);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
#insightVideoModal #recommendedList::-webkit-scrollbar-track {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
#insightVideoModal .comment-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 220px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
#insightVideoModal .comment-resizer {
|
||
touch-action: none;
|
||
user-select: none;
|
||
}
|
||
|
||
#insightVideoModal .comment-list-wrap {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
#insightVideoModal .comment-box {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 1024px 이하: 댓글 패널(슬라이드업) 토글 */
|
||
@media (max-width: 1024px) {
|
||
#insightVideoModal .comment-wrap {
|
||
height: min(48vh, 420px);
|
||
}
|
||
|
||
#insightVideoModal .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;
|
||
}
|
||
|
||
#insightVideoModal.is-comment-open .comment-wrap {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- ============================================ -->
|
||
<!-- 영상 모달 (video.php 구조 참고) -->
|
||
<!-- ============================================ -->
|
||
<div class="modal video" id="insightVideoModal" 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="insight-bookmark-chk">
|
||
<input id="insight-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">×</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 = $('#insightVideoModal');
|
||
var currentContentId = 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('[Insight] Player Error:', event.data);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
|
||
createPlayer();
|
||
} else {
|
||
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;
|
||
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 >= 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('[Insight] Save Learning Failed:', result.message);
|
||
}
|
||
}).catch(function (e) {
|
||
console.error('[Insight] 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.attr('data-category-name') || $item.find('.item-category').text().trim() || '인사이트';
|
||
var subCat = $item.data('sub-category') || $item.attr('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
|
||
});
|
||
});
|
||
|
||
// ── 상단 배너 클릭 → 동일 모달 열기 ──
|
||
$(document).on('click', '.hero-banner .banner-video-trigger', function (e) {
|
||
e.preventDefault();
|
||
|
||
var $banner = $(this);
|
||
var contentId = String($banner.data('content-id') || $banner.attr('data-content-id') || '');
|
||
var videoUrl = String($banner.data('content-url') || $banner.attr('data-content-url') || '');
|
||
var title = String($banner.data('title') || $banner.attr('data-title') || '').trim();
|
||
var desc = String($banner.data('description') || $banner.attr('data-description') || '');
|
||
var catName = String($banner.data('category-name') || $banner.attr('data-category-name') || '인사이트');
|
||
var subCat = String($banner.data('sub-category') || $banner.attr('data-sub-category') || '');
|
||
var watchTm = parseInt($banner.data('watch-tm') || $banner.attr('data-watch-tm') || '0', 10) || 0;
|
||
var contentTm = parseInt($banner.data('content-tm') || $banner.attr('data-content-tm') || '0', 10) || 0;
|
||
|
||
// 동일 content_id 카드가 리스트에 있으면 해당 메타(카테고리/그룹/설명/진도)를 우선 사용
|
||
var $card = $('#video-list .video-item[data-content-id="' + contentId + '"]');
|
||
if ($card.length) {
|
||
videoUrl = String($card.data('content-url') || $card.attr('data-content-url') || videoUrl);
|
||
title = String($card.data('title') || $card.attr('data-title') || title).trim();
|
||
desc = String($card.data('description') || $card.attr('data-description') || desc);
|
||
catName = String($card.data('category-name') || $card.attr('data-category-name') || catName);
|
||
subCat = String($card.data('sub-category') || $card.attr('data-sub-category') || subCat);
|
||
watchTm = parseInt($card.data('watch-tm') || $card.attr('data-watch-tm') || String(watchTm), 10) || 0;
|
||
contentTm = parseInt($card.data('content-tm') || $card.attr('data-content-tm') || String(contentTm), 10) || 0;
|
||
}
|
||
|
||
if (!contentId || !videoUrl) {
|
||
console.warn('[Insight Banner] content_id 또는 video_url이 없습니다.');
|
||
return;
|
||
}
|
||
|
||
var isBookmarked = $card.length ? $card.find('.bookmark input[type="checkbox"]').is(':checked') : undefined;
|
||
|
||
openModal({
|
||
contentId: contentId,
|
||
videoUrl: videoUrl,
|
||
title: title,
|
||
description: desc,
|
||
categoryName: catName,
|
||
subCategory: subCat,
|
||
watchTm: watchTm,
|
||
contentTm: contentTm,
|
||
bookmark: isBookmarked
|
||
});
|
||
});
|
||
|
||
// ── 리스트 카드 북마크 토글/저장 ──
|
||
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"]')) {
|
||
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('.insightCommentResize');
|
||
}
|
||
|
||
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.insightCommentResize touchmove.insightCommentResize', onResizerPointerMove)
|
||
.on('mouseup.insightCommentResize touchend.insightCommentResize touchcancel.insightCommentResize', 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;
|
||
lastSaveTime = 0;
|
||
var baseWatchTm = Math.floor(info.watchTm || 0);
|
||
lastCurrentTime = baseWatchTm;
|
||
|
||
// YouTube IFrame API 플레이어 세팅
|
||
var videoId = extractVideoId(info.videoUrl);
|
||
if (videoId) {
|
||
// 배너 진입 시 0초 재생을 피하기 위해 서버 watch_tm 확정 후 1회만 초기화
|
||
$.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) {
|
||
var serverWatchTm = 0;
|
||
if (res && res.success && res.watch_tm > 0) {
|
||
serverWatchTm = parseInt(res.watch_tm, 10) || 0;
|
||
$('#video-list .video-item[data-content-id="' + info.contentId + '"]').attr('data-watch-tm', serverWatchTm);
|
||
}
|
||
|
||
var startTm = Math.max(baseWatchTm, serverWatchTm);
|
||
lastCurrentTime = startTm;
|
||
initYTPlayer(videoId, startTm);
|
||
},
|
||
error: function() {
|
||
lastCurrentTime = baseWatchTm;
|
||
initYTPlayer(videoId, baseWatchTm);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 영상 정보
|
||
$modal.find('#modalTitle').text(info.title);
|
||
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());
|
||
$('#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;
|
||
|
||
// 댓글 토글 리스너/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);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── 추천영상 리스트 구성 (현재 페이지 영상 목록 활용) ──
|
||
function buildRecommendedList(excludeContentId) {
|
||
var $list = $modal.find('#recommendedList');
|
||
$list.empty();
|
||
var count = 0;
|
||
|
||
$('#video-list .video-item').each(function () {
|
||
var $item = $(this);
|
||
var cid = $item.data('content-id') || $item.attr('data-content-id') || '';
|
||
if (!cid || String(cid) === String(excludeContentId)) return;
|
||
if (count >= 10) return false;
|
||
|
||
var videoUrl = $item.data('content-url') || $item.attr('data-content-url') || '';
|
||
var title = $item.data('title') || $item.find('.item-title').text().trim() || '';
|
||
var catName = $item.data('category-name') || $item.attr('data-category-name') || $item.find('.item-category').text().trim() || '인사이트';
|
||
var thumbSrc = $item.find('.item-thumb img, .thumb img').attr('src') || '/img/video/img_thumb_01.png';
|
||
var desc = String($item.data('description') || '');
|
||
var subCat = $item.data('sub-category') || $item.attr('data-sub-category') || '';
|
||
var wTm = parseInt($item.data('watch-tm') || $item.attr('data-watch-tm') || '0', 10);
|
||
var cTm = parseInt($item.data('content-tm') || $item.attr('data-content-tm') || '0', 10);
|
||
|
||
var li =
|
||
'<li>' +
|
||
'<a href="#" class="list" ' +
|
||
'data-content-id="' + cid + '" ' +
|
||
'data-content-url="' + videoUrl.replace(/"/g, '"') + '" ' +
|
||
'data-title="' + title.replace(/"/g, '"') + '" ' +
|
||
'data-description="' + desc.replace(/"/g, '"') + '" ' +
|
||
'data-category-name="' + catName.replace(/"/g, '"') + '" ' +
|
||
'data-sub-category="' + subCat.replace(/"/g, '"') + '" ' +
|
||
'data-watch-tm="' + wTm + '" ' +
|
||
'data-content-tm="' + cTm + '">' +
|
||
'<div class="thumb"><img src="' + thumbSrc + '" alt="" /></div>' +
|
||
'<div class="txt-box">' +
|
||
'<div class="category leader">' + catName + '</div>' +
|
||
'<div class="title">' + title + '</div>' +
|
||
'</div>' +
|
||
'</a>' +
|
||
'</li>';
|
||
$list.append(li);
|
||
count++;
|
||
});
|
||
|
||
if (count === 0) {
|
||
$list.html('<li class="empty">추천 영상이 없습니다.</li>');
|
||
}
|
||
|
||
setTimeout(adjustRecommendedListHeight, 0);
|
||
}
|
||
|
||
// ── 추천영상 클릭 → 영상 전환 ──
|
||
$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') || $a.attr('data-category-name') || '인사이트',
|
||
subCategory: $a.data('sub-category') || $a.attr('data-sub-category') || '',
|
||
watchTm: parseInt($a.data('watch-tm') || '0', 10),
|
||
contentTm: parseInt($a.data('content-tm') || '0', 10)
|
||
});
|
||
});
|
||
|
||
$(window).on('resize', function () {
|
||
if ($modal.is(':visible')) {
|
||
adjustRecommendedListHeight();
|
||
}
|
||
});
|
||
|
||
// ══════════════════════════════════════════════
|
||
// 댓글 CRUD
|
||
// ══════════════════════════════════════════════
|
||
|
||
// ── 댓글 목록 로드 ──
|
||
function loadComments(contentId) {
|
||
var $commentList = $modal.find('.comment-list');
|
||
if (!contentId) return;
|
||
|
||
$.getJSON(API_BASE + '/get_comments.php', { content_id: contentId })
|
||
.done(function (data) {
|
||
if (data.success && Array.isArray(data.data)) {
|
||
renderComments(data.data);
|
||
} else {
|
||
$commentList.html('<li class="empty">등록된 댓글이 없습니다.</li>');
|
||
}
|
||
})
|
||
.fail(function () {
|
||
$commentList.html('<li class="empty">댓글을 불러올 수 없습니다.</li>');
|
||
});
|
||
}
|
||
|
||
// ── 댓글 렌더링 (온보딩 VideoModalBase 동일 구조) ──
|
||
function renderComments(comments) {
|
||
var $commentList = $modal.find('.comment-list');
|
||
if (!comments.length) {
|
||
$commentList.html('<li class="empty-comment" style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">작성된 댓글이 없습니다.</li>');
|
||
return;
|
||
}
|
||
|
||
var html = comments.map(function (c) {
|
||
var safeComment = (c.comment || '')
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
var safeName = (c.member_name || '익명')
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
var actions = c.is_author
|
||
? '<div class="actions">' +
|
||
'<button type="button" class="btn-edit-comment" data-id="' + c.id + '">수정</button>' +
|
||
'<button type="button" class="btn-del-comment" data-id="' + c.id + '">삭제</button>' +
|
||
'</div>'
|
||
: '';
|
||
|
||
return '<li data-id="' + c.id + '">' +
|
||
'<div class="comment-info">' +
|
||
'<div class="photo"><img src="' + (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('등록');
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|