Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+964
View File
@@ -0,0 +1,964 @@
<!doctype html>
<html lang="ko">
<head>
<?php include(__DIR__ . "/_include/_head.php") ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
</head>
<?php
$SET_PREFIX = 'L'; //L=리더십(기본값), I=인사이트
$activeCate = isset($_GET['cate']) ? trim($_GET['cate']) : '';
require_once __DIR__ . '/../bbs/category_init_data.php';
?>
<body>
<div class="wrap leadership">
<?php include(__DIR__ . "/_include/_header.php") ?>
<!-- container -->
<div class="container">
<!-- editor's pick -->
<section class="leadership-hero">
<div class="leadership-inner hero-top hero-breadcrumb">
<ul class="breadcrumb" aria-label="breadcrumb">
<li><a href="./index.php">홈</a></li>
<li><a href="./leadership.php">리더십</a></li>
<li><span class="current"></span></li>
</ul>
</div>
<!-- editor's pick -->
<!-- 리더십 고정 배너 3종: PC 1442×460 / 모바일 360×234 -->
<div class="hero-banner">
<div class="swiper hero-swiper">
<div class="swiper-wrapper">
<?php foreach ($array_banner_img as $i => $bn): ?>
<div class="swiper-slide">
<div class="banner-img">
<picture>
<source media="(min-width: 769px)" srcset="<?= $bn['normal'] ?>">
<source media="(max-width: 768px)" srcset="<?= $bn['mobile'] ?>">
<img src="<?= $bn['normal'] ?>" alt="<?= $bn['title'] ?>">
</picture>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="swiper-pagination hero-pagination" aria-hidden="true"></div>
</div>
</section>
<!-- Tabs -->
<div class="leadership-tabs" aria-label="카테고리">
<div class="leadership-inner">
<div class="leadership-tabs-list" role="tablist" aria-label="카테고리 탭">
<?php foreach ($array_tab_info as $i => $ti):
$isActive = ($activeCate !== '') ? ($ti['base_code'] === $activeCate) : ($i === 0);
?>
<button class="leadership-tab <?php if($isActive){?>is-active<?php }?>" type="button" role="tab" aria-selected="<?php if($isActive){?>true<?php }else{?>false<?php }?> " data-cate="<?= $ti['base_code'] ?>">
<span class="tab-icon"><img src="<?= $ti['src'] ?>" alt="" /></span>
<span class="tab-text"><?= $ti['code_name'] ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- video list -->
<section class="leadership-video-list">
<div class="leadership-inner">
<div class="list-head">
<span class="total">TOTAL <em></em></span>
<div class="list-options">
<div class="select-wrap">
<select class="select-sort" title="정렬">
<option value="view" selected>조회수</option>
<option value="latest" >업데이트</option>
<option value="seen">내가본컨텐츠</option>
<option value="unseen">안본컨텐츠</option>
</select>
</div>
</div>
</div>
<!-- 탭선택에 따른 동적 컨텐츠영역 -->
<ul class="video-grid" id="video-list"></ul>
</div>
</section>
</div>
<!-- // container -->
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Hero Swiper
new Swiper('.hero-swiper', {
loop: true,
autoplay: {
delay: 4000,
disableOnInteraction: false,
},
speed: 600,
pagination: {
el: '.hero-pagination',
clickable: true,
},
});
// Tabs
const tabs = Array.from(document.querySelectorAll('.leadership-tab'));
if (!tabs.length) return;
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
tabs.forEach(function (t) {
t.classList.remove('is-active');
t.setAttribute('aria-selected', 'false');
});
let currentText = $(this).find('.tab-text').text();
console.log('tabs');
console.log(currentText);
$('.current').text(currentText);
tab.classList.add('is-active');
tab.setAttribute('aria-selected', 'true');
});
});
});
</script>
<script>
$( document ).ready(function() {
/*
let currentText = $('.current').find('.tab-text').text();
if(currentText==""){$('.current').text("리더십 입문");}
*/
let firstTabText = $('.leadership-tab.is-active').find('.tab-text').text();
if($('.current').text() == "" && firstTabText != ""){
$('.current').text(firstTabText);
}
});
$(function () {
let page = 1;
let loading = false;
let lastPage = false;
let requestSeq = 0;
//let category = $('.leadership-tab.is-active').data('cate') || 'CA200L01';
var urlParams = new URLSearchParams(window.location.search);
var urlCate = urlParams.get('cate') || '';
if (urlCate) {
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
var $matchTab = $('.leadership-tab[data-cate="' + urlCate + '"]');
if ($matchTab.length) {
$matchTab.addClass('is-active').attr('aria-selected', 'true');
}
}
let category = urlCate || $('.leadership-tab.is-active').data('cate') || 'CA200<?= $SET_PREFIX ?>01';
let sort = $('.select-sort').val() || 'latest';
loadVideos();
$('.leadership-tab').on('click', function () {
if (loading) return;
$('.leadership-tab').removeClass('is-active').attr('aria-selected', 'false');
$(this).addClass('is-active').attr('aria-selected', 'true');
category = $(this).data('cate');
resetList(true);
loadVideos();
});
$('.select-sort').on('change', function () {
if (loading) return;
sort = $(this).val();
resetList(true);
loadVideos();
});
$('.container').on('scroll', function () {
if (loading || lastPage) return;
const scrollTop = this.scrollTop;
const windowHeight = this.clientHeight;
const docHeight = this.scrollHeight;
if (scrollTop + windowHeight >= docHeight - 100) {
page++;
loadVideos();
}
});
function resetList(moveTop) {
page = 1;
lastPage = false;
$('#video-list').empty();
if (moveTop) {
$('.container').scrollTop(0);
}
}
function setLoading(isLoading) {
loading = isLoading;
}
function loadVideos() {
requestSeq++;
const currentRequestSeq = requestSeq;
setLoading(true);
$.ajax({
url: '/ajax/get_video_list.php?SET_PREFIX=<?= $SET_PREFIX ?>',
type: 'GET',
dataType: 'json',
cache: false,
data: {
category: category,
sort: sort,
page: page
},
success: function (res) {
if (currentRequestSeq !== requestSeq) return;
if (!res || res.success !== true) {
if (page > 1) page--;
return;
}
const html = $.trim(res.html || '');
const totalCount = parseInt(res.total_count, 10) || 0;
$('.total em').text(totalCount);
if (page === 1) {
$('#video-list').html(html);
if (html === '') {
lastPage = true;
$('#video-list').html(
'<li class="video-item video-empty">' +
'<div class="item-info">' +
'<strong class="item-title">등록된 콘텐츠가 없습니다.</strong>' +
'</div>' +
'</li>'
);
}
} else {
if (html === '') {
lastPage = true;
page--;
} else {
$('#video-list').append(html);
}
}
},
error: function () {
if (page > 1) page--;
},
complete: function () {
if (currentRequestSeq === requestSeq) {
setLoading(false);
}
}
});
}
});
</script>
<!-- ============================================ -->
<!-- 영상 모달 댓글 스타일 (온보딩 동일) -->
<!-- ============================================ -->
<style>
/* #leadershipVideoModal .comment-list { list-style: none; margin: 0; padding: 0; }
#leadershipVideoModal .comment-list li { padding: 0; border-bottom: none; }
#leadershipVideoModal .comment-list li.empty-comment { padding: 20px; text-align: center; color: rgba(255,255,255,0.4); font-size: 13px; }
#leadershipVideoModal .comment-list .comment-info { display: flex; align-items: flex-start; }
#leadershipVideoModal .comment-list .comment-info .photo { flex-shrink: 0; margin-right: 6px; display: flex; align-items: center; }
#leadershipVideoModal .comment-list .comment-info .photo img { width: 20px; height: 20px; border-radius: 50%; }
#leadershipVideoModal .comment-list .comment-info .user-comment { flex: 1; min-width: 0; display: flex; align-items: center; }
#leadershipVideoModal .comment-list .comment-info .user-comment .user-name { font-size: 13px; color: #ccc; white-space: nowrap; flex-shrink: 0; margin-right: 8px; }
#leadershipVideoModal .comment-list .comment-info .user-comment .user-text { flex: 1; background: transparent; border: none; color: #fff; font-size: 13px; line-height: 1.4; resize: none; padding: 10px 0 0 0; word-break: break-word; font-family: inherit; }
#leadershipVideoModal .comment-list .comment-info .user-comment .user-text:disabled { opacity: 1; cursor: default; padding-top: 10px;}
#leadershipVideoModal .comment-list .comment-info .user-comment .user-text:not(:disabled) { border: 1px solid #555; border-radius: 4px; padding: 4px 8px; background: #2a2a2a; }
#leadershipVideoModal .comment-list .comment-info .actions { display: flex; gap: 4px; align-items: center; flex-shrink: 0; margin-left: 8px; align-self: center; }
#leadershipVideoModal .comment-list .comment-info .actions button { padding: 3px 8px; font-size: 12px; border: 1px solid #555; border-radius: 3px; background: #333; color: #ddd; cursor: pointer; white-space: nowrap; }
#leadershipVideoModal .comment-list .comment-info .actions button:hover { background: #555; color: #fff; }
#leadershipVideoModal .comment-list li.editing .user-text { border: 1px solid #00ffcc !important; } */
</style>
<!-- ============================================ -->
<!-- 영상 모달 (video.php 구조 참고) -->
<!-- ============================================ -->
<div class="modal video" id="leadershipVideoModal" style="display:none">
<div class="modal-content">
<div class="modal-body">
<div class="video-contents">
<div class="video-area">
<div class="video-box">
<div id="videoPlayer"></div>
</div>
</div>
<div class="video-info">
<div class="tit-box">
<div class="meta">
<span id="modalCategoryName"></span>
<em id="modalSubCategory"></em>
</div>
<h3 id="modalTitle"></h3>
<label class="bookmark" for="leadership-bookmark-chk">
<input id="leadership-bookmark-chk" type="checkbox" value="" />
<span>북마크</span>
</label>
</div>
<div class="desc" id="modalDesc"></div>
</div>
</div>
<div class="video-side">
<div class="video-header">
<h5 class="tit">관련영상</h5>
<!-- <span class="badge" id="modalBadge">리더십</span> -->
<span class="close">&times;</span>
</div>
<div class="video-list">
<ul id="recommendedList">
<!-- JS로 동적 렌더링 -->
</ul>
</div>
<div class="comment-wrap">
<!-- 댓글 목록 -->
<div class="comment-list-wrap">
<ul class="comment-list">
<li class="empty-comment" style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">작성된 댓글이 없습니다.</li>
</ul>
</div>
<!-- 댓글 입력 -->
<div class="comment-box">
<textarea placeholder="댓글을 작성해주세요"></textarea>
<div class="btn-area">
<button class="btn-cancel" disabled>취소</button>
<button class="btn-save" disabled>등록</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ============================================ -->
<!-- 영상 모달 JS (클릭 → 모달 열기 / 추천영상 / 댓글) -->
<!-- ============================================ -->
<script>
$(function () {
var API_BASE = '/bbs/api';
var $modal = $('#leadershipVideoModal');
var currentContentId = null;
var editingCommentId = null;
// ── YouTube IFrame API 시청 추적 변수 ──
var ytPlayer = null;
var trackingInterval = null;
var lastSaveTime = 0;
var lastCurrentTime = 0;
// YouTube IFrame API 스크립트 로드
if (!document.getElementById('youtube-iframe-api')) {
var ytScript = document.createElement('script');
ytScript.id = 'youtube-iframe-api';
ytScript.src = 'https://www.youtube.com/iframe_api';
ytScript.async = true;
document.head.appendChild(ytScript);
}
// ── YouTube 비디오 ID 추출 ──
function extractVideoId(url) {
if (!url) return '';
var videoId = url;
if (url.indexOf('youtube.com/watch') > -1) {
try { videoId = new URL(url).searchParams.get('v') || url; } catch (e) {}
} else if (url.indexOf('youtu.be/') > -1) {
videoId = url.split('youtu.be/')[1].split(/[?&#]/)[0];
} else if (url.indexOf('youtube.com/embed/') > -1) {
videoId = url.split('youtube.com/embed/')[1].split(/[?&#]/)[0];
}
if (videoId && videoId.indexOf('/') === -1 && videoId.indexOf('.') === -1) {
return videoId;
}
return '';
}
// ── YT.Player 생성 ──
function initYTPlayer(videoId, startSeconds) {
destroyYTPlayer();
startSeconds = Math.floor(startSeconds || 0);
var createPlayer = function () {
ytPlayer = new YT.Player('videoPlayer', {
height: '100%',
width: '100%',
videoId: videoId,
playerVars: {
autoplay: 1,
controls: 1,
rel: 0,
modestbranding: 1,
enablejsapi: 1,
start: startSeconds
},
events: {
'onReady': function (event) {
if (event.target && typeof event.target.playVideo === 'function') {
event.target.playVideo();
}
},
'onStateChange': function (event) {
handlePlayerStateChange(event);
},
'onError': function (event) {
console.error('[Leadership] Player Error:', event.data);
}
}
});
};
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
createPlayer();
} else {
// YT API 아직 로드 안 됨 → 대기
var checkYt = setInterval(function () {
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
clearInterval(checkYt);
createPlayer();
}
}, 500);
setTimeout(function () { clearInterval(checkYt); }, 10000);
}
}
// ── 플레이어 파괴 ──
function destroyYTPlayer() {
stopTracking();
if (ytPlayer && typeof ytPlayer.destroy === 'function') {
try { ytPlayer.destroy(); } catch (e) {}
}
ytPlayer = null;
// YT.Player가 div를 iframe으로 교체하므로 다시 div를 복원
var $box = $modal.find('.video-box');
if (!$box.find('#videoPlayer').length) {
$box.html('<div id="videoPlayer"></div>');
}
}
// ── 플레이어 상태 변경 핸들러 (VideoModalBase 패턴) ──
function handlePlayerStateChange(event) {
if (event.data === YT.PlayerState.PLAYING) {
startTracking();
} else {
stopTracking();
if (event.data === YT.PlayerState.PAUSED || event.data === YT.PlayerState.ENDED) {
saveLearningProgress(Math.floor(lastSaveTime), 'N');
lastSaveTime = 0;
}
}
}
// ── 시청 추적 시작 (1초 주기) ──
function startTracking() {
if (trackingInterval) return;
lastSaveTime = 0;
trackingInterval = setInterval(function () {
if (!ytPlayer || typeof ytPlayer.getCurrentTime !== 'function') return;
var playerState = (typeof ytPlayer.getPlayerState === 'function')
? ytPlayer.getPlayerState() : null;
if (playerState !== YT.PlayerState.PLAYING) return;
var currentTime = ytPlayer.getCurrentTime();
var diff = currentTime - lastCurrentTime;
if (Math.abs(diff) <= 5) {
lastSaveTime += 1;
}
lastCurrentTime = currentTime;
// 5초마다 서버에 heartbeat 전송
if (lastSaveTime >= 5) {
saveLearningProgress(lastSaveTime);
lastSaveTime = 0;
}
}, 1000);
}
// ── 시청 추적 중지 ──
function stopTracking() {
if (trackingInterval) {
clearInterval(trackingInterval);
trackingInterval = null;
}
}
// ── 서버에 학습 진행상황 저장 (save_learning.php) ──
function saveLearningProgress(heartbeatSeconds, isWatching) {
if (!ytPlayer || !currentContentId) return;
if (typeof ytPlayer.getCurrentTime !== 'function') return;
var currentTime = Math.floor(ytPlayer.getCurrentTime());
var duration = Math.floor(ytPlayer.getDuration());
var incrementalWatch = Math.max(0, Math.floor(heartbeatSeconds || 0));
var params = new URLSearchParams({
content_id: String(currentContentId),
watch_tm: String(Math.max(0, currentTime)),
content_tm: String(Math.max(0, duration)),
all_tm_increment: String(incrementalWatch),
is_watching: isWatching === 'N' ? 'N' : 'Y'
});
fetch(API_BASE + '/save_learning.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: params.toString(),
keepalive: true
}).then(function (r) { return r.json(); }).then(function (result) {
if (!result.success) {
console.error('[Leadership] Save Learning Failed:', result.message);
}
}).catch(function (e) {
console.error('[Leadership] Save Learning Error:', e);
});
}
// ── 영상 카드 클릭 → 모달 열기 ──
$(document).on('click', '#video-list .video-item .card-link', function (e) {
e.preventDefault();
var $item = $(this).closest('.video-item');
var contentId = $item.data('content-id') || $item.attr('data-content-id') || '';
var videoUrl = $item.data('content-url') || $item.attr('data-content-url') || '';
var title = $item.data('title') || $item.find('.item-title').text().trim() || '';
var desc = $item.data('description') || '';
var catName = $item.data('category-name') || $item.find('.item-category').text().trim() || '리더십';
var subCat = $item.data('sub-category') || '';
var watchTm = parseInt($item.data('watch-tm') || $item.attr('data-watch-tm') || '0', 10);
var contentTm = parseInt($item.data('content-tm') || $item.attr('data-content-tm') || '0', 10);
var isBookmarked = $item.find('.bookmark input[type="checkbox"]').is(':checked');
if (!contentId) {
console.warn('[VideoModal] content-id가 없습니다.');
return;
}
openModal({
contentId: contentId,
videoUrl: videoUrl,
title: title,
description: desc,
categoryName: catName,
subCategory: subCat,
watchTm: watchTm,
contentTm: contentTm,
bookmark: isBookmarked
});
});
// ── 모달 열기 ──
function openModal(info) {
currentContentId = info.contentId;
editingCommentId = null;
lastSaveTime = 0;
lastCurrentTime = Math.floor(info.watchTm || 0);
// YouTube IFrame API 플레이어 세팅
var videoId = extractVideoId(info.videoUrl);
if (videoId) {
initYTPlayer(videoId, info.watchTm || 0);
}
// 서버에서 최신 watch_tm 조회 후 재생 위치 보정
$.ajax({
url: '/bbs/myclass/api/get_video_time.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content_id: info.contentId }),
dataType: 'json',
success: function(res) {
if (res.success && res.watch_tm > 0) {
var serverWatchTm = parseInt(res.watch_tm, 10);
if (ytPlayer && typeof ytPlayer.seekTo === 'function' && serverWatchTm > (info.watchTm || 0)) {
ytPlayer.seekTo(serverWatchTm, true);
lastCurrentTime = serverWatchTm;
}
// 카드 data-watch-tm 동기화
$('#video-list .video-item[data-content-id="' + info.contentId + '"]').attr('data-watch-tm', serverWatchTm);
}
}
});
// 영상 정보
$modal.find('#modalTitle').text(info.title);
// 설명: 줄바꿈을 <br>로 변환하여 표시
var descHtml = $('<div>').text(info.description || '').html().replace(/\n/g, '<br>');
$modal.find('#modalDesc').html(descHtml);
$modal.find('#modalCategoryName').text(info.categoryName || '리더십');
$modal.find('#modalSubCategory').text(info.subCategory || '');
$modal.find('#modalBadge').text(info.categoryName || '리더십');
// 추천영상 리스트
buildRecommendedList(info.contentId);
// 댓글 로드
loadComments(info.contentId);
resetCommentForm();
// 북마크 상태 동기화
syncBookmark(info.contentId, info.bookmark);
// 모달 표시
$modal.show();
$('body').css('overflow', 'hidden');
}
// ── 모달 닫기 (진행상황 저장 후 닫기) ──
function closeModal() {
// 마지막 시청 진행상황 저장
stopTracking();
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
var currentTime = Math.floor(ytPlayer.getCurrentTime());
// 카드의 data-watch-tm 업데이트 (재오픈 시 적용)
$('#video-list .video-item[data-content-id="' + currentContentId + '"]').attr('data-watch-tm', currentTime);
saveLearningProgress(Math.floor(lastSaveTime), 'N');
}
lastSaveTime = 0;
destroyYTPlayer();
$modal.hide();
$('body').css('overflow', '');
currentContentId = null;
editingCommentId = null;
}
$modal.on('click', '.close', closeModal);
$modal.on('click', function (e) {
if ($(e.target).hasClass('modal')) closeModal();
});
$(document).on('keydown', function (e) {
if (e.key === 'Escape' && $modal.is(':visible')) closeModal();
});
// ── 북마크 동기화 ──
function syncBookmark(contentId, isBookmarked) {
var $chk = $modal.find('.bookmark input[type="checkbox"]');
if (!$chk.length) return;
if (isBookmarked !== undefined) {
$chk.prop('checked', !!isBookmarked);
} else {
// 서버에서 북마크 상태 조회
$.ajax({
url: '/bbs/myclass/api/get_video_time.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content_id: contentId }),
dataType: 'json',
success: function(res) {
if (res.success) {
$chk.prop('checked', !!res.is_bookmarked);
}
}
});
}
}
// ── 북마크 토글 이벤트 ──
$modal.on('change', '.bookmark input[type="checkbox"]', function () {
var $chk = $(this);
var isActive = $chk.is(':checked') ? '1' : '0';
if (!currentContentId) return;
$chk.prop('disabled', true);
$.ajax({
url: '/bbs/api/save_wishlist.php',
type: 'POST',
data: { content_id: String(currentContentId), is_active: isActive },
dataType: 'json',
success: function (res) {
if (!res || res.success !== true) {
$chk.prop('checked', !$chk.is(':checked'));
alert(res?.message || '북마크 저장에 실패했습니다.');
} else {
// 목록 카드의 북마크 상태도 동기화
var $card = $('#video-list .video-item[data-content-id="' + currentContentId + '"] .bookmark input');
$card.prop('checked', isActive === '1');
}
},
error: function () {
$chk.prop('checked', !$chk.is(':checked'));
alert('북마크 저장 중 오류가 발생했습니다.');
},
complete: function () {
$chk.prop('disabled', false);
}
});
});
// ── 추천영상 리스트 구성 (키워드 기반 API) ──
function buildRecommendedList(contentId) {
var $list = $modal.find('#recommendedList');
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">추천 영상 로딩 중...</li>');
if (!contentId) {
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
return;
}
$.getJSON(API_BASE + '/get_recommend_videos.php', { content_id: contentId })
.done(function (data) {
if (!data.success || !data.videos || data.videos.length === 0) {
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
// 키워드 배지 숨기기
$modal.find('#modalBadge').hide();
return;
}
// 키워드 배지 업데이트
if (data.keywords && data.keywords.length > 0) {
$modal.find('#modalBadge').text(data.keywords.join(', ')).show();
} else {
$modal.find('#modalBadge').hide();
}
$list.empty();
var catClassMap = {
'CA10001': 'myclass', 'CA10002': 'onboarding', 'CA10003': 'legal',
'CA10004': 'leader', 'CA10005': 'insight', 'CA10006': 'biztrend'
};
data.videos.forEach(function (v) {
var safeTitle = $('<div>').text(v.title || '').html();
var safeCat = $('<div>').text(v.category_name || '').html();
var thumb = v.thumbnail || '/img/video/img_thumb_01.png';
var catClass = catClassMap[v.category_code] || '';
var li =
'<li>' +
'<a href="#" class="list" ' +
'data-content-id="' + (v.content_id || '') + '" ' +
'data-content-url="' + (v.content_url || '').replace(/"/g, '&quot;') + '" ' +
'data-title="' + (v.title || '').replace(/"/g, '&quot;') + '" ' +
'data-description="' + (v.description || '').replace(/"/g, '&quot;') + '" ' +
'data-category-name="' + (v.category_name || '').replace(/"/g, '&quot;') + '">' +
'<div class="thumb"><img src="' + thumb + '" alt="" /></div>' +
'<div class="txt-box">' +
'<div class="category ' + catClass + '">' + safeCat + '</div>' +
'<div class="title">' + safeTitle + '</div>' +
'</div>' +
'</a>' +
'</li>';
$list.append(li);
});
})
.fail(function () {
$list.html('<li style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">관련 추천 영상이 없습니다.</li>');
});
}
// ── 추천영상 클릭 → 영상 전환 ──
$modal.on('click', '#recommendedList .list', function (e) {
e.preventDefault();
var $a = $(this);
// 현재 영상 진행상황 저장 후 전환
stopTracking();
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
saveLearningProgress(Math.floor(lastSaveTime), 'N');
}
lastSaveTime = 0;
openModal({
contentId: $a.data('content-id'),
videoUrl: $a.data('content-url') || '',
title: $a.data('title') || $a.find('.title').text().trim(),
description: $a.data('description') || '',
categoryName: $a.data('category-name') || '리더십'
});
});
// ══════════════════════════════════════════════
// 댓글 CRUD
// ══════════════════════════════════════════════
// ── 댓글 목록 로드 ──
function loadComments(contentId) {
var $commentList = $modal.find('.comment-list');
if (!contentId) return;
$.getJSON(API_BASE + '/get_comments.php', { content_id: contentId })
.done(function (data) {
if (data.success && Array.isArray(data.data)) {
renderComments(data.data);
} else {
$commentList.html('<li class="empty">등록된 댓글이 없습니다.</li>');
}
})
.fail(function () {
$commentList.html('<li class="empty">댓글을 불러올 수 없습니다.</li>');
});
}
// ── 댓글 렌더링 (온보딩 VideoModalBase 동일 구조) ──
function renderComments(comments) {
var $commentList = $modal.find('.comment-list');
if (!comments.length) {
$commentList.html('<li class="empty-comment" style="padding:20px; text-align:center; color:rgba(255,255,255,0.4); font-size:13px;">작성된 댓글이 없습니다.</li>');
return;
}
var html = comments.map(function (c) {
var safeComment = (c.comment || '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
var safeName = (c.member_name || '익명')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
var actions = c.is_author
? '<div class="actions">' +
'<button type="button" class="btn-edit-comment" data-id="' + c.id + '">수정</button>' +
'<button type="button" class="btn-del-comment" data-id="' + c.id + '">삭제</button>' +
'</div>'
: '';
return '<li data-id="' + c.id + '">' +
'<div class="comment-info">' +
'<div class="photo"><img src="/img/ico/ico_user.svg" /></div>' +
'<div class="user-comment">' +
'<span class="user-name">' + safeName + '</span>' +
'<textarea class="user-text" disabled>' + safeComment + '</textarea>' +
'</div>' +
actions +
'</div>' +
'</li>';
}).join('');
$commentList.html(html);
adjustUserTextHeights();
}
// ── 댓글 textarea 높이 자동 조정 ──
function adjustUserTextHeights() {
$modal.find('.user-text').each(function () {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
this.style.overflow = 'hidden';
});
}
// ── 댓글 인라인 수정 (온보딩 동일) ──
$modal.on('click', '.btn-edit-comment', function (e) {
e.preventDefault();
var $li = $(this).closest('li');
var $textarea = $li.find('.user-text');
var $btn = $(this);
if ($btn.text() === '수정') {
$textarea.prop('disabled', false).focus();
$btn.text('저장').css('color', '#00ffcc');
$li.addClass('editing');
} else {
var newComment = $textarea.val().trim();
if (!newComment) return;
var commentId = $btn.data('id');
$.ajax({
url: API_BASE + '/save_comment.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content_id: currentContentId, comment: newComment, id: commentId }),
success: function (data) {
if (data.success) {
$btn.text('수정').css('color', '');
$textarea.prop('disabled', true);
$li.removeClass('editing');
loadComments(currentContentId);
} else {
alert(data.message || '저장에 실패했습니다.');
}
},
error: function () { alert('저장 중 오류가 발생했습니다.'); }
});
}
});
// ── 댓글 삭제 (온보딩 동일) ──
$modal.on('click', '.btn-del-comment', function (e) {
e.preventDefault();
if (!confirm('정말 삭제하시겠습니까?')) return;
var commentId = $(this).data('id');
$.ajax({
url: API_BASE + '/delete_comment.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id: commentId }),
success: function (data) {
if (data.success) {
loadComments(currentContentId);
} else {
alert(data.message || '삭제에 실패했습니다.');
}
},
error: function () { alert('삭제 중 오류가 발생했습니다.'); }
});
});
// ── 새 댓글 작성 ──
$modal.on('click', '.comment-box .btn-save', function () {
var $textarea = $modal.find('.comment-box textarea');
var comment = $textarea.val().trim();
if (!comment || !currentContentId) return;
var payload = { content_id: currentContentId, comment: comment };
$.ajax({
url: API_BASE + '/save_comment.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: function (data) {
if (data.success) {
resetCommentForm();
loadComments(currentContentId);
} else {
alert(data.message || '저장에 실패했습니다.');
}
},
error: function () { alert('저장 중 오류가 발생했습니다.'); }
});
});
// ── 댓글 취소 ──
$modal.on('click', '.comment-box .btn-cancel', function () {
resetCommentForm();
});
// ── textarea 입력 → 버튼 활성화 ──
$modal.on('input', '.comment-box textarea', function () {
var hasText = $(this).val().trim().length > 0;
$modal.find('.btn-cancel').prop('disabled', !hasText);
$modal.find('.btn-save').prop('disabled', !hasText);
});
function resetCommentForm() {
$modal.find('.comment-box textarea').val('');
$modal.find('.btn-cancel').prop('disabled', true);
$modal.find('.btn-save').prop('disabled', true).text('등록');
editingCommentId = null;
}
});
</script>
</body>
</html>