/* -- ===================================== -- 영상 모달 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('
'); } } // ── 플레이어 상태 변경 핸들러 (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); // 설명: 줄바꿈을