/* -- ===================================== -- 영상 모달 JS (클릭 → 모달 열기 / 추천영상 / 댓글) -- ===================================== */ $(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('
'); } } // ── 플레이어 상태 변경 핸들러 (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 categoryCode = $item.data('category-code') || $item.attr('data-category-code') || ''; console.warn('[VideoModal] category-code='+categoryCode); 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 }); }); /* $(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); // 설명: 줄바꿈을
로 변환하여 표시 var descHtml = $('
').text(info.description || '').html().replace(/\n/g, '
'); $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') || '/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 = '
  • ' + '' + '
    ' + '
    ' + '
    ' + catName + '
    ' + '
    ' + title + '
    ' + '
    ' + '
    ' + '
  • '; $list.append(li); count++; }); if (count === 0) { $list.html('
  • 추천 영상이 없습니다.
  • '); } } // ── 추천영상 클릭 → 영상 전환 ── $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('
  • 등록된 댓글이 없습니다.
  • '); } }) .fail(function () { $commentList.html('
  • 댓글을 불러올 수 없습니다.
  • '); }); } // ── 댓글 렌더링 (온보딩 모달 동일 구조) ── function renderComments(comments) { var $commentList = $modal.find('.comment-list'); if (!comments.length) { $commentList.html('
  • 등록된 댓글이 없습니다.
  • '); return; } var userIconSvg = ''; var html = comments.map(function (c) { var safeComment = (c.comment || '') .replace(/&/g, '&').replace(//g, '>') .replace(/\n/g, '
    '); var safeName = (c.member_name || '익명') .replace(/&/g, '&').replace(//g, '>'); var buttons = c.is_author ? '
    ' + '' + '' + '
    ' : ''; return '
  • ' + '
    ' + '' + userIconSvg + '' + '' + safeName + '' + '
    ' + '
    ' + '
    ' + safeComment + '
    ' + buttons + '
    ' + '' + '
  • '; }).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; } });