온라인 교육 사이트나 강의 플랫폼을 운영하고 계신가요? 사용자가 비디오를 얼마나 시청했는지 추적하고 시각적으로 표시하고 싶으신가요?
이 글에서는 JetEngine과 Elementor를 활용해 실시간으로 비디오 시청 진행률을 추적하고 진행률 바로 표시하는 방법을 단계별로 알려드리겠습니다.
🎯 완성 후 기대 효과
- ✅ 10분 영상을 5분 시청 시 정확히 50% 진행률 표시
- ✅ 사용자별 개별 진행률 저장 및 복원
- ✅ 페이지를 새로고침해도 진행률 유지
- ✅ 이전 시청 지점부터 이어보기 기능
- ✅ 실시간 진행률 바 업데이트
📋 사전 준비사항
시작하기 전에 다음 플러그인들이 설치되어 있어야 합니다:
- ✅ Elementor Pro (또는 무료 버전)
- ✅ JetEngine
- ✅ JetElements
- ✅ 기본적인 PHP/JavaScript 지식
🏗️ 1단계: 데이터베이스 준비
먼저 비디오 진행률 데이터를 저장할 구조를 만들어야 합니다.
JetEngine에서 커스텀 포스트 타입 생성
- WordPress 관리자 → JetEngine → Post Types 이동
- Add New 클릭
- 다음과 같이 설정:
- Post Type Name:
video_course
- Slug:
video-course
- Menu Icon:
dashicons-video-alt3
- Post Type Name:
메타 필드 추가
JetEngine → Meta Boxes → Add New에서 다음 필드들을 추가합니다:
필드명 | 타입 | 설명 |
---|---|---|
video_url | URL | 비디오 파일 URL |
video_title | Text | 비디오 제목 |
💡 팁:
- 메타박스는 위에서 생성한
video_course
포스트 타입에만 적용되도록 설정하세요.- 비디오 길이는 자동으로 가져옵니다! JavaScript의
video.duration
속성을 사용해 자동 추출하므로 별도 필드가 불필요합니다.
💻 2단계: 백엔드 PHP 코드 구현
테마의 functions.php
파일에 다음 코드를 추가합니다:
<?php
/**
* 비디오 진행률 추적 시스템
* functions.php에 추가
*/
// 스크립트 로딩 및 AJAX 설정
function enqueue_video_progress_scripts() {
wp_enqueue_script('jquery');
wp_enqueue_script(
'video-progress-tracker',
get_template_directory_uri() . '/js/video-progress.js',
array('jquery'),
'1.0.0',
true
);
// JavaScript에 필요한 데이터 전달
wp_localize_script('video-progress-tracker', 'ajax_object', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('video_progress_nonce')
));
wp_localize_script('video-progress-tracker', 'wp_data', array(
'post_id' => get_the_ID(),
'user_id' => get_current_user_id()
));
}
add_action('wp_enqueue_scripts', 'enqueue_video_progress_scripts');
// 데이터베이스 테이블 생성
function create_video_progress_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'video_progress';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
post_id bigint(20) NOT NULL,
current_time float NOT NULL,
duration float NOT NULL,
percentage int(3) NOT NULL,
last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY user_post (user_id, post_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
add_action('after_switch_theme', 'create_video_progress_table');
// 진행률 저장 AJAX 핸들러
function save_video_progress_handler() {
if (!wp_verify_nonce($_POST['nonce'], 'video_progress_nonce')) {
wp_die('보안 검증 실패');
}
global $wpdb;
$table_name = $wpdb->prefix . 'video_progress';
$user_id = intval($_POST['user_id']);
$post_id = intval($_POST['post_id']);
$current_time = floatval($_POST['current_time']);
$duration = floatval($_POST['duration']);
$percentage = intval($_POST['percentage']);
// 기존 레코드 확인 후 업데이트 또는 생성
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT id FROM $table_name WHERE user_id = %d AND post_id = %d",
$user_id, $post_id
));
if ($existing) {
$result = $wpdb->update(
$table_name,
array(
'current_time' => $current_time,
'duration' => $duration,
'percentage' => $percentage
),
array('user_id' => $user_id, 'post_id' => $post_id),
array('%f', '%f', '%d'),
array('%d', '%d')
);
} else {
$result = $wpdb->insert(
$table_name,
array(
'user_id' => $user_id,
'post_id' => $post_id,
'current_time' => $current_time,
'duration' => $duration,
'percentage' => $percentage
),
array('%d', '%d', '%f', '%f', '%d')
);
}
if ($result !== false) {
wp_send_json_success(array('percentage' => $percentage));
} else {
wp_send_json_error('저장 실패');
}
}
add_action('wp_ajax_save_video_progress', 'save_video_progress_handler');
add_action('wp_ajax_nopriv_save_video_progress', 'save_video_progress_handler');
// 진행률 불러오기 AJAX 핸들러
function get_video_progress_handler() {
if (!wp_verify_nonce($_POST['nonce'], 'video_progress_nonce')) {
wp_die('보안 검증 실패');
}
global $wpdb;
$table_name = $wpdb->prefix . 'video_progress';
$user_id = intval($_POST['user_id']);
$post_id = intval($_POST['post_id']);
$progress = $wpdb->get_row($wpdb->prepare(
"SELECT current_time, duration, percentage FROM $table_name WHERE user_id = %d AND post_id = %d",
$user_id, $post_id
));
if ($progress) {
wp_send_json_success(array(
'current_time' => $progress->current_time,
'duration' => $progress->duration,
'percentage' => $progress->percentage
));
} else {
wp_send_json_success(array('percentage' => 0));
}
}
add_action('wp_ajax_get_video_progress', 'get_video_progress_handler');
add_action('wp_ajax_nopriv_get_video_progress', 'get_video_progress_handler');
// 비디오 메타데이터 자동 저장 AJAX 핸들러 (선택사항)
function save_video_metadata_handler() {
if (!wp_verify_nonce($_POST['nonce'], 'video_progress_nonce')) {
wp_die('보안 검증 실패');
}
$post_id = intval($_POST['post_id']);
$duration = floatval($_POST['duration']);
$width = intval($_POST['width']);
$height = intval($_POST['height']);
// 메타 필드에 자동으로 저장
if ($duration > 0) {
update_post_meta($post_id, 'video_duration', $duration);
}
if ($width > 0 && $height > 0) {
update_post_meta($post_id, 'video_width', $width);
update_post_meta($post_id, 'video_height', $height);
update_post_meta($post_id, 'video_resolution', $width . 'x' . $height);
}
wp_send_json_success('메타데이터 저장 완료');
}
add_action('wp_ajax_save_video_metadata', 'save_video_metadata_handler');
add_action('wp_ajax_nopriv_save_video_metadata', 'save_video_metadata_handler');
// JetEngine 동적 태그 등록
function get_user_video_progress($post_id = null) {
if (!$post_id) $post_id = get_the_ID();
$user_id = get_current_user_id();
if (!$user_id) return 0;
global $wpdb;
$table_name = $wpdb->prefix . 'video_progress';
$progress = $wpdb->get_var($wpdb->prepare(
"SELECT percentage FROM $table_name WHERE user_id = %d AND post_id = %d",
$user_id, $post_id
));
return $progress ? $progress : 0;
}
// JetEngine 매크로 등록
if (function_exists('jet_engine')) {
function register_video_progress_dynamic_tag() {
jet_engine()->listings->macros->register_callback('video_progress', 'get_user_video_progress');
}
add_action('init', 'register_video_progress_dynamic_tag');
}
?>
⚡ 3단계: JavaScript 프론트엔드 구현
테마 폴더에 js/video-progress.js
파일을 생성하고 다음 코드를 추가합니다:
jQuery(document).ready(function($) {
const video = document.querySelector('video');
const progressBar = document.querySelector('.jet-progress-bar__fill');
const progressText = document.querySelector('.jet-progress-bar__percent');
const postId = wp_data.post_id;
const userId = wp_data.user_id;
if (!video) return;
// 페이지 로드 시 저장된 진행률 불러오기
loadSavedProgress();
// 비디오 메타데이터 로드 시 자동으로 길이 정보 저장
video.addEventListener('loadedmetadata', function() {
const duration = video.duration;
console.log('비디오 총 길이:', duration, '초');
// 비디오 메타데이터를 서버에 자동 저장 (선택사항)
saveVideoMetadata(postId, duration, video.videoWidth, video.videoHeight);
});
// 비디오 재생 시간 업데이트 시마다 실행
video.addEventListener('timeupdate', function() {
const currentTime = video.currentTime;
const duration = video.duration;
if (duration > 0) {
const progress = Math.round((currentTime / duration) * 100);
updateProgressBar(progress);
// 5초마다 서버에 저장
if (Math.floor(currentTime) % 5 === 0) {
saveProgress(postId, userId, currentTime, duration, progress);
}
}
});
// 비디오 종료 시
video.addEventListener('ended', function() {
updateProgressBar(100);
saveProgress(postId, userId, video.duration, video.duration, 100);
});
// 일시정지 시 현재 진행률 저장
video.addEventListener('pause', function() {
const currentTime = video.currentTime;
const duration = video.duration;
const progress = Math.round((currentTime / duration) * 100);
saveProgress(postId, userId, currentTime, duration, progress);
});
// 진행률 바 업데이트 함수
function updateProgressBar(percentage) {
if (progressBar) {
progressBar.style.width = percentage + '%';
}
if (progressText) {
progressText.textContent = percentage + '%';
}
}
// 서버에 진행률 저장
function saveProgress(postId, userId, currentTime, duration, percentage) {
$.ajax({
url: ajax_object.ajax_url,
type: 'POST',
data: {
action: 'save_video_progress',
post_id: postId,
user_id: userId,
current_time: currentTime,
duration: duration,
percentage: percentage,
nonce: ajax_object.nonce
},
success: function(response) {
if (response.success) {
console.log('진행률 저장:', percentage + '%');
}
}
});
}
// 저장된 진행률 불러오기
function loadSavedProgress() {
$.ajax({
url: ajax_object.ajax_url,
type: 'POST',
data: {
action: 'get_video_progress',
post_id: postId,
user_id: userId,
nonce: ajax_object.nonce
},
success: function(response) {
if (response.success && response.data.percentage) {
const savedPercentage = response.data.percentage;
const savedTime = response.data.current_time;
updateProgressBar(savedPercentage);
// 이어보기 옵션 제공
if (savedPercentage > 5 && savedPercentage < 100) {
if (confirm('이전 시청 지점(' + savedPercentage + '%)부터 계속 보시겠습니까?')) {
video.currentTime = savedTime;
}
}
}
}
});
}
// 비디오 메타데이터 자동 저장 (선택사항)
function saveVideoMetadata(postId, duration, width, height) {
$.ajax({
url: ajax_object.ajax_url,
type: 'POST',
data: {
action: 'save_video_metadata',
post_id: postId,
duration: duration,
width: width,
height: height,
nonce: ajax_object.nonce
},
success: function(response) {
if (response.success) {
console.log('비디오 메타데이터 자동 저장 완료');
}
}
});
}
});
🎨 4단계: Elementor에서 화면 구성
비디오 위젯 추가
- Elementor 편집기에서 페이지 편집
- Video 위젯 드래그 앤 드롭
- Source → Dynamic 선택
- Dynamic Tags → Meta Data →
video_url
선택
진행률 바 위젯 추가
- JetElements에서 Progress Bar 위젯 검색
- 위젯을 비디오 아래에 배치
- 다음과 같이 설정:
- Title: “시청 진행률”
- Percentage:
{% video_progress %}
입력 - Type: “Bar Inside” 선택
- Animation: “On” (선택사항)
![Elementor 설정 화면 스크린샷 자리]
고급 설정
Advanced 탭에서:
- CSS Classes:
video-progress-bar
추가 - Custom CSS (선택사항):
.video-progress-bar .jet-progress-bar__fill {
background: linear-gradient(90deg, #4CAF50, #8BC34A);
transition: width 0.3s ease;
}
.video-progress-bar .jet-progress-bar__percent {
font-weight: bold;
color: #333;
}
🎯 5단계: JetEngine 리스팅 설정 (선택사항)
리스팅 그리드 생성
- JetEngine → Listings → Add New
- Listing Source: 앞서 생성한
video_course
선택 - Type: “Template” 선택
동적 콘텐츠 추가
리스팅 템플릿에서:
- 동적 필드 위젯으로
video_title
표시 - Video 위젯으로
video_url
표시 - Progress Bar 위젯으로
{% video_progress %}
표시
🧪 6단계: 테스트 및 검증
기능 테스트 체크리스트
- [ ] 비디오 재생 시 진행률 바 실시간 업데이트
- [ ] 페이지 새로고침 후 진행률 유지
- [ ] 일시정지 시 진행률 저장
- [ ] 비디오 완료 시 100% 표시
- [ ] 이어보기 팝업 정상 작동
- [ ] 사용자별 개별 진행률 저장
브라우저 콘솔 확인
F12 → Console에서 다음 메시지들이 정상적으로 출력되는지 확인:
진행률 저장: XX%
비디오 재생 시작
비디오 시청 완료!
🚨 문제 해결 가이드
진행률이 저장되지 않는 경우
- AJAX 오류 확인
// 콘솔에서 네트워크 탭 확인 // 500 에러: PHP 문법 오류 // 403 에러: nonce 검증 실패
- 데이터베이스 테이블 확인
-- phpMyAdmin에서 실행 SELECT * FROM wp_video_progress;
- 파일 경로 확인
video-progress.js
파일이 올바른 위치에 있는지functions.php
에 문법 오류가 없는지
진행률 바가 업데이트되지 않는 경우
- CSS 선택자 확인
- 브라우저 개발자 도구로 실제 클래스명 확인
- JetElements 버전에 따라 클래스명이 다를 수 있음
- 동적 태그 확인
{% video_progress %}
가 올바르게 입력되었는지- JetEngine 매크로가 정상 등록되었는지
🚀 고급 기능 추가하기
비디오 메타데이터 자동 추출
// 비디오가 로드되면 자동으로 메타데이터 추출
video.addEventListener('loadedmetadata', function() {
const metadata = {
duration: video.duration,
width: video.videoWidth,
height: video.videoHeight,
aspect_ratio: (video.videoWidth / video.videoHeight).toFixed(2)
};
console.log('추출된 메타데이터:', metadata);
// 자동으로 포스트 메타에 저장
saveVideoMetadata(postId, metadata);
});
이점:
- ✅ 관리자가 수동으로 입력할 필요 없음
- ✅ 정확한 비디오 길이 자동 계산
- ✅ 해상도 정보로 반응형 플레이어 최적화
시청 완료 시 자동 진행
// 시청 완료 시 다음 강의로 자동 이동
function redirect_to_next_video() {
// 현재 포스트의 다음 포스트 가져오기
$next_post = get_next_post();
if ($next_post) {
return get_permalink($next_post->ID);
}
return false;
}
시청 시간 통계
// 사용자별 총 시청 시간 계산
function get_user_total_watch_time($user_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'video_progress';
$total_time = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(current_time) FROM $table_name WHERE user_id = %d",
$user_id
));
return $total_time ? $total_time : 0;
}
진행률별 뱃지 시스템
// 진행률에 따른 뱃지 표시
function get_progress_badge($percentage) {
if ($percentage >= 100) return '🏆 완주';
if ($percentage >= 75) return '🥈 거의 다';
if ($percentage >= 50) return '🥉 반 완주';
if ($percentage >= 25) return '📚 진행중';
return '▶️ 시작';
}
📊 성능 최적화 팁
데이터베이스 최적화
- 인덱스 추가
ALTER TABLE wp_video_progress ADD INDEX idx_user_post (user_id, post_id);
- 오래된 데이터 정리
// 30일 이상 된 데이터 삭제 function cleanup_old_progress_data() { global $wpdb; $table_name = $wpdb->prefix . 'video_progress'; $wpdb->query( "DELETE FROM $table_name WHERE last_updated < DATE_SUB(NOW(), INTERVAL 30 DAY)" ); }
프론트엔드 최적화
- 저장 빈도 조절: 5초마다 저장 (너무 자주 저장하면 서버 부하)
- 로컬 캐싱: 진행률을 로컬스토리지에도 저장
- 압축 및 최소화: JavaScript 파일 압축
📱 모바일 최적화
반응형 진행률 바
@media (max-width: 768px) {
.video-progress-bar {
margin: 10px 0;
}
.jet-progress-bar__percent {
font-size: 14px;
}
}
터치 이벤트 고려
// 모바일에서 터치 이벤트도 고려
video.addEventListener('touchend', function() {
// 터치 종료 시에도 진행률 저장
saveCurrentProgress();
});
🔒 보안 고려사항
데이터 검증 강화
// 입력값 검증 추가
function validate_progress_data($current_time, $duration, $percentage) {
// 음수 값 체크
if ($current_time < 0 || $duration < 0 || $percentage < 0) {
return false;
}
// 논리적 범위 체크
if ($percentage > 100 || $current_time > $duration) {
return false;
}
return true;
}
SQL 인젝션 방지
- 모든 데이터베이스 쿼리에
$wpdb->prepare()
사용 - 사용자 입력값 sanitize 처리
🎉 마무리
축하합니다! 이제 워드프레스 사이트에서 완전히 작동하는 비디오 진행률 추적 시스템을 구축했습니다.
구현된 기능들
✅ 실시간 진행률 추적: 비디오 시청과 동시에 진행률 업데이트
✅ 개인별 데이터 저장: 사용자마다 독립적인 진행률 관리
✅ 이어보기 기능: 이전 시청 지점부터 계속 시청 가능
✅ 시각적 진행률 바: JetElements로 아름다운 진행률 표시
✅ 자동 저장: 일시정지나 페이지 이탈 시 자동 저장
다음 단계
이제 이 시스템을 기반으로 다음과 같은 기능들을 추가해보세요:
- 🏆 수료증 발급 시스템
- 📊 학습 진도 대시보드
- 🎮 게이미피케이션 요소
- 📱 모바일 앱 연동
💬 마지막으로
이 가이드가 도움이 되셨나요? 댓글로 여러분의 구현 경험을 공유해주세요!
막히는 부분이 있으시면 언제든 질문해주시고, 더 나은 기능 아이디어가 있으시면 함께 발전시켜 나가요! 🚀
태그: #워드프레스 #JetEngine #Elementor #비디오추적 #온라인교육 #LMS #웹개발
공유하기: 이 포스팅이 유용했다면 SNS에 공유해주세요! 💪