Files
edu/js/apply_page/back/Copy of add_modal_01.js
T

484 lines
17 KiB
JavaScript

/*
-- =====================================
-- 영상 모달 JS (클릭 → 모달 열기 / 추천영상 / 댓글)
-- =====================================
*/
$(function () {
var API_BASE = '/edu/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);
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
});
});
// ── 모달 열기 ──
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);
}
// 영상 정보
$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();
// 모달 표시
$modal.show();
$('body').css('overflow', 'hidden');
}
// ── 모달 닫기 (진행상황 저장 후 닫기) ──
function closeModal() {
// 마지막 시청 진행상황 저장
stopTracking();
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
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 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; // 최대 10개
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.find('.item-category').text().trim() || '리더십';
var thumbSrc = $item.find('.item-thumb img, .thumb img').attr('src') || '/edu/img/video/img_thumb_01.png';
var desc = String($item.data('description') || '');
var subCat = $item.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, '&quot;') + '" ' +
'data-title="' + title.replace(/"/g, '&quot;') + '" ' +
'data-description="' + desc.replace(/"/g, '&quot;') + '" ' +
'data-category-name="' + catName.replace(/"/g, '&quot;') + '" ' +
'data-sub-category="' + subCat.replace(/"/g, '&quot;') + '" ' +
'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>');
}
}
// ── 추천영상 클릭 → 영상 전환 ──
$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') || '리더십',
subCategory: $a.data('sub-category') || '',
watchTm: parseInt($a.data('watch-tm') || '0', 10),
contentTm: parseInt($a.data('content-tm') || '0', 10)
});
});
// ══════════════════════════════════════════════
// 댓글 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>');
});
}
// ── 댓글 렌더링 (온보딩 모달 동일 구조) ──
function renderComments(comments) {
var $commentList = $modal.find('.comment-list');
if (!comments.length) {
$commentList.html('<li class="empty">등록된 댓글이 없습니다.</li>');
return;
}
var userIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>';
var html = comments.map(function (c) {
var safeComment = (c.comment || '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
var safeName = (c.member_name || '익명')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
var buttons = c.is_author
? '<div class="comment-actions">' +
'<button type="button" class="btn-edit" data-id="' + c.id + '">수정</button>' +
'<button type="button" class="btn-delete" data-id="' + c.id + '">삭제</button>' +
'</div>'
: '';
return '<li data-comment-id="' + c.id + '">' +
'<div class="comment-header">' +
'<span class="user-icon">' + userIconSvg + '</span>' +
'<span class="comment-author">' + safeName + '</span>' +
'</div>' +
'<div class="comment-body">' +
'<div class="comment-content">' + safeComment + '</div>' +
buttons +
'</div>' +
'<div class="comment-footer">' +
'<span class="comment-date">' + (c.created_at || '') + '</span>' +
'</div>' +
'</li>';
}).join('');
$commentList.html(html);
}
// ── 댓글 수정 ──
$modal.on('click', '.btn-edit', function () {
var $li = $(this).closest('li');
var content = $li.find('.comment-content').text();
editingCommentId = $(this).data('id');
var $textarea = $modal.find('.comment-box textarea');
$textarea.val(content).focus();
$modal.find('.btn-cancel').prop('disabled', false);
$modal.find('.btn-save').prop('disabled', false).text('수정');
});
// ── 댓글 삭제 ──
$modal.on('click', '.btn-delete', function () {
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 };
if (editingCommentId) payload.id = editingCommentId;
$.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;
}
});