그누보드5 기반 취미 강의 사이트 구현 가이드

동영상 관리, 학습 진도 추적, 결제 시스템 연동 등의 주요 기능 구현 방법

#1. 소개

그누보드5를 활용하여 취미 강의 사이트를 구축하는 것은 오픈 소스를 활용한 효율적인 웹사이트 개발 방법입니다. 이 가이드에서는 그누보드5를 기반으로 취미 강의 사이트를 구현하기 위한 PHP 코드 및 설정 방법을 상세히 다룹니다.

주요 기능

  • 회원 관리 시스템
  • 동영상 강의 관리
  • 학습 진도 추적
  • 결제 시스템 연동
  • 현대적이고 세련된 UI/UX

#2. 시스템 요구사항

그누보드5 기반 취미 강의 사이트를 구축하기 위한 시스템 요구사항입니다.

서버 환경

  • PHP 7.2 이상 (PHP 8.0 권장)
  • MySQL 5.6 이상 또는 MariaDB 10.0 이상
  • Apache 또는 Nginx 웹 서버
  • PHP 확장 모듈: mbstring, curl, gd, xml, zip

그누보드 요구사항

  • 그누보드5 최신 버전 (5.4.x 이상 권장)
  • 영카트5 (선택사항: 결제 기능 강화)
  • MySQL 또는 MariaDB 데이터베이스
  • PHP 메모리 제한: 128MB 이상

주의사항

동영상 서비스를 위해서는 충분한 스토리지와 대역폭이 필요합니다. 클라우드 기반 동영상 호스팅 서비스(Vimeo, AWS S3+CloudFront, 네이버 클라우드 등)를 활용하는 것이 권장됩니다.

#3. 그누보드5 설치 및 설정

3.1 그누보드5 설치

그누보드5를 설치하는 기본 절차입니다.

  1. 그누보드5 최신 버전을 공식 사이트에서 다운로드
  2. 서버에 그누보드5 파일 업로드
  3. 웹 브라우저로 설치 경로 접속 (예: https://yourdomain.com/g5/install.php)
  4. 데이터베이스 정보 및 관리자 정보 입력 후 설치 진행
  5. 설치 완료 후 install.php 파일 삭제

3.2 취미 강의 사이트 기본 설정

그누보드5 설치 후 취미 강의 사이트를 위한 기본 설정을 진행합니다.

// config.php 파일에 아래 내용 추가하여 동영상 관련 설정 추가
define('G5_VIDEO_DIR', 'video');
define('G5_VIDEO_URL', G5_URL.'/'.G5_VIDEO_DIR);
define('G5_VIDEO_PATH', G5_PATH.'/'.G5_VIDEO_DIR);

// 동영상 파일 업로드 용량 제한 설정 (1GB)
define('G5_VIDEO_MAX_SIZE', 1024*1024*1024);

// 허용 가능한 동영상 확장자
define('G5_VIDEO_EXTENSIONS', 'mp4,webm,mov');

// 동영상 강의 썸네일 설정
define('G5_LECTURE_THUMB_WIDTH', 300);
define('G5_LECTURE_THUMB_HEIGHT', 169);

관리자 설정에서 필요한 추가 설정:

  1. 관리자 → 환경설정 → 기본환경설정에서 사이트 정보 업데이트
  2. 관리자 → 환경설정 → 회원설정에서 회원가입 관련 설정 조정
  3. 필요한 게시판 및 메뉴 생성
  4. 동영상 파일 저장을 위한 디렉토리 생성 및 권한 설정

#4. 데이터베이스 구조 수정

취미 강의 사이트를 위한 추가 테이블을 생성합니다.

4.1 강의 관련 테이블

-- 강의 카테고리 테이블
CREATE TABLE `g5_lecture_category` (
  `ca_id` varchar(10) NOT NULL PRIMARY KEY,
  `ca_name` varchar(255) NOT NULL,
  `ca_order` int(11) NOT NULL DEFAULT 0,
  `ca_use` tinyint(4) NOT NULL DEFAULT 1,
  `ca_img` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 강의 정보 테이블
CREATE TABLE `g5_lecture` (
  `lt_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `ca_id` varchar(10) NOT NULL,
  `lt_name` varchar(255) NOT NULL,
  `lt_content` text NOT NULL,
  `lt_thumbnail` varchar(255) DEFAULT NULL,
  `lt_price` int(11) NOT NULL DEFAULT 0,
  `lt_discount` int(11) NOT NULL DEFAULT 0,
  `lt_duration` int(11) NOT NULL COMMENT '강의 시간(분)',
  `lt_level` tinyint(4) NOT NULL DEFAULT 1 COMMENT '난이도 (1-5)',
  `lt_order` int(11) NOT NULL DEFAULT 0,
  `lt_use` tinyint(4) NOT NULL DEFAULT 1,
  `lt_date` datetime NOT NULL DEFAULT current_timestamp(),
  `lt_update` datetime DEFAULT NULL ON UPDATE current_timestamp(),
  `lt_view_count` int(11) NOT NULL DEFAULT 0,
  KEY `ca_id` (`ca_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 강의 동영상 테이블
CREATE TABLE `g5_lecture_video` (
  `video_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `lt_id` int(11) NOT NULL,
  `video_title` varchar(255) NOT NULL,
  `video_file` varchar(255) NOT NULL,
  `video_url` varchar(255) DEFAULT NULL COMMENT '외부 동영상 URL (Vimeo, YouTube 등)',
  `video_type` varchar(10) NOT NULL DEFAULT 'file' COMMENT 'file, youtube, vimeo',
  `video_duration` int(11) NOT NULL DEFAULT 0 COMMENT '초 단위',
  `video_order` int(11) NOT NULL DEFAULT 0,
  `video_use` tinyint(4) NOT NULL DEFAULT 1,
  `video_content` text DEFAULT NULL,
  `video_date` datetime NOT NULL DEFAULT current_timestamp(),
  KEY `lt_id` (`lt_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 학습 진도 추적을 위한 테이블

-- 강의 구매 내역 테이블
CREATE TABLE `g5_lecture_purchase` (
  `purchase_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `mb_id` varchar(20) NOT NULL,
  `lt_id` int(11) NOT NULL,
  `purchase_price` int(11) NOT NULL,
  `purchase_date` datetime NOT NULL DEFAULT current_timestamp(),
  `purchase_status` varchar(10) NOT NULL DEFAULT 'active' COMMENT 'active, expired, cancelled',
  `expire_date` datetime DEFAULT NULL COMMENT '수강 기간 만료일',
  KEY `mb_id` (`mb_id`),
  KEY `lt_id` (`lt_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 학습 진도 테이블
CREATE TABLE `g5_lecture_progress` (
  `progress_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `mb_id` varchar(20) NOT NULL,
  `lt_id` int(11) NOT NULL,
  `video_id` int(11) NOT NULL,
  `progress_time` int(11) NOT NULL DEFAULT 0 COMMENT '마지막 시청 위치(초)',
  `is_completed` tinyint(4) NOT NULL DEFAULT 0,
  `complete_date` datetime DEFAULT NULL,
  `last_update` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  KEY `mb_id_lt_id` (`mb_id`,`lt_id`),
  KEY `mb_id_video_id` (`mb_id`,`video_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

#5. 동영상 관리 시스템

5.1 동영상 업로드 및 관리 기능

관리자 페이지에 강의 및 동영상 관리 기능을 추가합니다.

/* 관리자 메뉴를 추가하는 PHP 코드 (adm/admin.menu300.php 파일에 추가) */
$menu['menu300'][] = array(
    '300100', 
    '강의 카테고리 관리', 
    G5_ADMIN_URL.'/lecture_category_list.php', 
    'lecture_category'
);
$menu['menu300'][] = array(
    '300200', 
    '강의 관리', 
    G5_ADMIN_URL.'/lecture_list.php', 
    'lecture'
);
$menu['menu300'][] = array(
    '300300', 
    '동영상 관리', 
    G5_ADMIN_URL.'/lecture_video_list.php', 
    'lecture_video'
);

5.2 동영상 업로드 처리 예제

강의 동영상 업로드 처리를 위한 PHP 코드입니다.

// adm/lecture_video_form_update.php

 G5_VIDEO_MAX_SIZE) {
        alert('파일 크기가 너무 큽니다. 최대 '.number_format(G5_VIDEO_MAX_SIZE/1024/1024).'MB까지 업로드 가능합니다.');
    }
    
    // 동영상 저장 디렉토리가 없으면 생성
    @mkdir(G5_VIDEO_PATH, G5_DIR_PERMISSION);
    @chmod(G5_VIDEO_PATH, G5_DIR_PERMISSION);
    
    // 파일명 변경 (고유한 이름으로)
    $video_file = 'video_'.$lt_id.'_'.time().'.'.$ext;
    $upload_file = G5_VIDEO_PATH.'/'.$video_file;
    
    // 동영상 파일 업로드
    if (move_uploaded_file($_FILES['video_file']['tmp_name'], $upload_file)) {
        @chmod($upload_file, G5_FILE_PERMISSION);
        
        // 이전 파일 삭제
        if ($old_video_file && file_exists(G5_VIDEO_PATH.'/'.$old_video_file)) {
            @unlink(G5_VIDEO_PATH.'/'.$old_video_file);
        }
    } else {
        alert('파일 업로드에 실패했습니다.');
    }
} else if ($video_type == 'file') {
    $video_file = $old_video_file;
}

// 동영상 정보 저장
if ($video_id) {
    // 기존 동영상 정보 수정
    $sql = "UPDATE g5_lecture_video SET
                lt_id = '$lt_id',
                video_title = '".sql_real_escape_string($video_title)."',
                video_content = '".sql_real_escape_string($video_content)."',
                video_type = '$video_type',
                video_url = '".sql_real_escape_string($video_url)."',
                video_duration = '$video_duration',
                video_order = '$video_order',
                video_use = '$video_use'";
    
    if ($video_file) {
        $sql .= ", video_file = '$video_file'";
    }
    
    $sql .= " WHERE video_id = '$video_id'";
    sql_query($sql);

    goto_url('./lecture_video_form.php?w=u&video_id='.$video_id.'<_id='.$lt_id);
} else {
    // 새 동영상 정보 추가
    $sql = "INSERT INTO g5_lecture_video
                (lt_id, video_title, video_content, video_file, video_url, 
                video_type, video_duration, video_order, video_use, video_date)
            VALUES
                ('$lt_id', '".sql_real_escape_string($video_title)."', 
                '".sql_real_escape_string($video_content)."', '$video_file', 
                '".sql_real_escape_string($video_url)."', '$video_type', 
                '$video_duration', '$video_order', '$video_use', '".G5_TIME_YMDHIS."')";
    sql_query($sql);
    $video_id = sql_insert_id();

    goto_url('./lecture_video_form.php?w=u&video_id='.$video_id.'<_id='.$lt_id);
}
?>

5.3 동영상 플레이어 구현

동영상 강의를 재생하기 위한 플레이어 구현 예제입니다.

// mobile/lecture/player.php

 NOW())";
$purchase = sql_fetch($sql);

// 무료 강의가 아닌데 구매 내역이 없으면 결제 페이지로 이동
if ($video['lt_price'] > 0 && !$purchase) {
    alert('해당 강의를 구매해야 시청할 수 있습니다.', G5_URL.'/mobile/lecture/detail.php?lt_id='.$video['lt_id']);
}

// 학습 진도 정보 가져오기
$sql = "SELECT * FROM g5_lecture_progress 
        WHERE mb_id = '{$member['mb_id']}' 
        AND video_id = '$video_id'";
$progress = sql_fetch($sql);

$last_position = 0;
if ($progress) {
    $last_position = $progress['progress_time'];
} else {
    // 진도 정보가 없으면 새로 생성
    $sql = "INSERT INTO g5_lecture_progress 
            (mb_id, lt_id, video_id, progress_time, is_completed, last_update) 
            VALUES 
            ('{$member['mb_id']}', '{$video['lt_id']}', '$video_id', 0, 0, NOW())";
    sql_query($sql);
}

$g5['title'] = $video['video_title'].' - '.$video['lt_name'];
include_once(G5_MOBILE_PATH.'/head.php');
?>

00:00 /

#6. 학습 진도 추적

6.1 진도 저장을 위한 AJAX 처리

학습 진도를 저장하기 위한 AJAX 처리 파일입니다.

// mobile/lecture/ajax.save_progress.php

 'error', 'message' => '로그인이 필요합니다.']));
}

$video_id = isset($_POST['video_id']) ? (int) $_POST['video_id'] : 0;
$progress_time = isset($_POST['progress_time']) ? (int) $_POST['progress_time'] : 0;
$is_completed = isset($_POST['is_completed']) ? (int) $_POST['is_completed'] : 0;

if (!$video_id || $progress_time < 0) {
    die(json_encode(['result' => 'error', 'message' => '올바르지 않은 요청입니다.']));
}

// 동영상 정보 가져오기
$sql = "SELECT lt_id FROM g5_lecture_video WHERE video_id = '$video_id'";
$video = sql_fetch($sql);

if (!$video) {
    die(json_encode(['result' => 'error', 'message' => '동영상 정보가 없습니다.']));
}

// 기존 진도 정보 확인
$sql = "SELECT * FROM g5_lecture_progress 
        WHERE mb_id = '{$member['mb_id']}' 
        AND video_id = '$video_id'";
$progress = sql_fetch($sql);

if ($progress) {
    // 진도 정보 업데이트
    $sql = "UPDATE g5_lecture_progress SET 
                progress_time = '$progress_time',
                last_update = NOW()";
    
    // 완료 상태 업데이트
    if ($is_completed && !$progress['is_completed']) {
        $sql .= ", is_completed = 1, complete_date = NOW()";
    } else if ($is_completed) {
        $sql .= ", is_completed = 1";
    }
    
    $sql .= " WHERE mb_id = '{$member['mb_id']}' AND video_id = '$video_id'";
    sql_query($sql);
} else {
    // 새 진도 정보 생성
    $complete_date = $is_completed ? ", complete_date = NOW()" : "";
    
    $sql = "INSERT INTO g5_lecture_progress 
                (mb_id, lt_id, video_id, progress_time, is_completed, last_update{$complete_date}) 
            VALUES 
                ('{$member['mb_id']}', '{$video['lt_id']}', '$video_id', 
                '$progress_time', '$is_completed', NOW()" . ($is_completed ? ", NOW()" : "") . ")";
    sql_query($sql);
}

// 강의 진도율 계산 및 업데이트 (전체 동영상 중 완료한 비율)
$sql = "SELECT COUNT(*) as total FROM g5_lecture_video WHERE lt_id = '{$video['lt_id']}' AND video_use = 1";
$total_videos = sql_fetch($sql);

$sql = "SELECT COUNT(*) as completed 
        FROM g5_lecture_progress 
        WHERE mb_id = '{$member['mb_id']}' 
        AND lt_id = '{$video['lt_id']}' 
        AND is_completed = 1";
$completed_videos = sql_fetch($sql);

$total = $total_videos['total'];
$completed = $completed_videos['completed'];
$progress_rate = $total > 0 ? ($completed / $total) * 100 : 0;

// 응답 반환
echo json_encode([
    'result' => 'success',
    'progress_time' => $progress_time,
    'is_completed' => $is_completed,
    'progress_rate' => $progress_rate
]);
?>

6.2 학습 진도율 표시

회원별 강의 진도율을 표시하는 기능입니다.

// mobile/lecture/my_courses.php



내 강의 목록

NOW()) ORDER BY p.purchase_date DESC"; $result = sql_query($sql); if (sql_num_rows($result) > 0) { while ($row = sql_fetch_array($result)) { // 강의 진도율 계산 $sql = "SELECT COUNT(*) as total FROM g5_lecture_video WHERE lt_id = '{$row['lt_id']}' AND video_use = 1"; $total_videos = sql_fetch($sql); $sql = "SELECT COUNT(*) as completed FROM g5_lecture_progress WHERE mb_id = '{$member['mb_id']}' AND lt_id = '{$row['lt_id']}' AND is_completed = 1"; $completed_videos = sql_fetch($sql); $total = $total_videos['total']; $completed = $completed_videos['completed']; $progress_rate = $total > 0 ? ($completed / $total) * 100 : 0; // 마지막 학습 정보 $sql = "SELECT v.video_title, p.last_update FROM g5_lecture_progress as p LEFT JOIN g5_lecture_video as v ON p.video_id = v.video_id WHERE p.mb_id = '{$member['mb_id']}' AND p.lt_id = '{$row['lt_id']}' ORDER BY p.last_update DESC LIMIT 1"; $last_learning = sql_fetch($sql); ?>
<?php echo $row['lt_name']; ?>
No Image

% 완료

마지막 학습:

구매한 강의가 없습니다.

강의 둘러보기

#7. 결제 시스템 연동

7.1 결제 시스템 구현

PG사 연동을 통한 강의 결제 시스템을 구현합니다. 여기서는 아임포트 결제 모듈을 예로 들겠습니다.

// mobile/lecture/payment.php

 NOW())";
$already_purchased = sql_fetch($sql);

if ($already_purchased) {
    alert('이미 구매한 강의입니다.', G5_URL.'/mobile/lecture/detail.php?lt_id='.$lt_id);
}

// 결제금액 계산
$price = $lecture['lt_price'];
if ($lecture['lt_discount'] > 0) {
    $price = $price - ($price * $lecture['lt_discount'] / 100);
}

$g5['title'] = $lecture['lt_name'].' - 결제하기';
include_once(G5_MOBILE_PATH.'/head.php');
?>

강의 결제하기

<?php echo $lecture['lt_name']; ?>
No Image

0) { ?>

% 할인

결제 방법 선택

주문 내용을 확인하였으며, 결제 진행에 동의합니다.

강의 구매 후 7일 이내에는 100% 환불이 가능합니다. 단, 50% 이상 수강 시에는 환불이 불가능합니다.

7.2 결제 검증 및 처리

결제가 완료된 후 서버에서 결제를 검증하고 처리하는 코드입니다.

// mobile/lecture/ajax.payment_verify.php

 'error', 'message' => '로그인이 필요합니다.']));
}

$lt_id = isset($_POST['lt_id']) ? (int) $_POST['lt_id'] : 0;
$price = isset($_POST['price']) ? (int) $_POST['price'] : 0;
$imp_uid = isset($_POST['imp_uid']) ? trim($_POST['imp_uid']) : '';
$merchant_uid = isset($_POST['merchant_uid']) ? trim($_POST['merchant_uid']) : '';

if (!$lt_id || !$price || !$imp_uid || !$merchant_uid) {
    die(json_encode(['result' => 'error', 'message' => '필수 파라미터가 누락되었습니다.']));
}

// 강의 정보 확인
$sql = "SELECT * FROM g5_lecture WHERE lt_id = '$lt_id' AND lt_use = 1";
$lecture = sql_fetch($sql);

if (!$lecture) {
    die(json_encode(['result' => 'error', 'message' => '존재하지 않는 강의입니다.']));
}

// 이미 구매한 강의인지 확인
$sql = "SELECT * FROM g5_lecture_purchase 
        WHERE mb_id = '{$member['mb_id']}' 
        AND lt_id = '$lt_id' 
        AND purchase_status = 'active'
        AND (expire_date IS NULL OR expire_date > NOW())";
$already_purchased = sql_fetch($sql);

if ($already_purchased) {
    die(json_encode(['result' => 'error', 'message' => '이미 구매한 강의입니다.']));
}

// 아임포트 결제 결과 확인
require_once(G5_PLUGIN_PATH.'/iamport/iamport.php');
$iamport = new Iamport($config['cf_iamport_api_key'], $config['cf_iamport_api_secret']);
$payment_data = $iamport->findByImpUID($imp_uid);

if (!$payment_data->success) {
    die(json_encode(['result' => 'error', 'message' => '결제 정보를 확인할 수 없습니다.']));
}

$payment = $payment_data->data;

// 결제 금액 검증
if ($payment->amount != $price) {
    die(json_encode(['result' => 'error', 'message' => '결제 금액이 일치하지 않습니다.']));
}

// 결제 상태 확인
if ($payment->status != 'paid') {
    die(json_encode(['result' => 'error', 'message' => '결제가 완료되지 않았습니다.']));
}

// 결제 정보 DB에 저장
$expire_date = null;
if (isset($lecture['lt_duration']) && $lecture['lt_duration'] > 0) {
    $expire_date = date("Y-m-d H:i:s", strtotime("+{$lecture['lt_duration']} days"));
    $expire_sql = ", expire_date = '$expire_date'";
} else {
    $expire_sql = ", expire_date = NULL";
}

$sql = "INSERT INTO g5_lecture_purchase 
            (mb_id, lt_id, purchase_price, purchase_date, purchase_status{$expire_sql}) 
        VALUES 
            ('{$member['mb_id']}', '$lt_id', '$price', NOW(), 'active'{$expire_sql})";
sql_query($sql);
$purchase_id = sql_insert_id();

// 결제 상세 정보 저장
$sql = "INSERT INTO g5_lecture_payment 
            (purchase_id, imp_uid, merchant_uid, pay_method, paid_amount, status, paid_at) 
        VALUES 
            ('$purchase_id', '{$payment->imp_uid}', '{$payment->merchant_uid}', 
            '{$payment->pay_method}', '{$payment->amount}', '{$payment->status}', 
            FROM_UNIXTIME({$payment->paid_at}))";
sql_query($sql);

// 회원 포인트 적립 (옵션)
if ($lecture['lt_point'] > 0) {
    insert_point($member['mb_id'], $lecture['lt_point'], "{$lecture['lt_name']} 강의 구매 포인트", "@lecture", $member['mb_id'], "{$lt_id}_{$purchase_id}");
}

// 응답 반환
echo json_encode([
    'result' => 'success',
    'purchase_id' => $purchase_id,
    'message' => '결제가 성공적으로 처리되었습니다.'
]);
?>

#8. 회원 관리 커스터마이징

8.1 회원 프로필 및 학습 정보 확장

회원 테이블에 학습 관련 정보를 추가하여 관리합니다.

-- 회원 테이블 확장 (ALTER TABLE 쿼리)
ALTER TABLE g5_member 
ADD COLUMN mb_lecture_count INT NOT NULL DEFAULT 0 COMMENT '구매한 강의 수',
ADD COLUMN mb_last_lecture INT DEFAULT NULL COMMENT '마지막 학습 강의 ID',
ADD COLUMN mb_last_video INT DEFAULT NULL COMMENT '마지막 학습 동영상 ID',
ADD COLUMN mb_last_study DATETIME DEFAULT NULL COMMENT '마지막 학습 시간';

회원 학습 정보 업데이트 함수:

// lib/lecture.lib.php

 NOW())";
    $row = sql_fetch($sql);
    $lecture_count = $row['cnt'];
    
    // 마지막 학습 정보 업데이트
    $sql = "SELECT p.lt_id, p.video_id, p.last_update
            FROM g5_lecture_progress as p
            WHERE p.mb_id = '$mb_id'
            ORDER BY p.last_update DESC
            LIMIT 1";
    $last_study = sql_fetch($sql);
    
    if ($last_study) {
        $mb_last_lecture = $last_study['lt_id'];
        $mb_last_video = $last_study['video_id'];
        $mb_last_study = $last_study['last_update'];
        
        $sql = "UPDATE g5_member SET 
                    mb_lecture_count = '$lecture_count',
                    mb_last_lecture = '$mb_last_lecture',
                    mb_last_video = '$mb_last_video',
                    mb_last_study = '$mb_last_study'
                WHERE mb_id = '$mb_id'";
    } else {
        $sql = "UPDATE g5_member SET 
                    mb_lecture_count = '$lecture_count',
                    mb_last_lecture = NULL,
                    mb_last_video = NULL,
                    mb_last_study = NULL
                WHERE mb_id = '$mb_id'";
    }
    
    sql_query($sql);
}

// 학습 활동 로깅 함수
function log_lecture_activity($mb_id, $lt_id, $video_id, $action) {
    $sql = "INSERT INTO g5_lecture_activity_log
                (mb_id, lt_id, video_id, action_type, log_datetime, ip_address)
            VALUES
                ('$mb_id', '$lt_id', '$video_id', '$action', NOW(), '{$_SERVER['REMOTE_ADDR']}')";
    sql_query($sql);
    
    // 회원 학습 정보 업데이트
    update_member_lecture_info($mb_id);
}
?>

8.2 회원 학습 통계 대시보드

회원 별 학습 통계를 보여주는 대시보드를 구현합니다.

// mobile/lecture/dashboard.php



나의 학습 대시보드

구매한 강의
완료한 강의
시간 총 학습 시간

최근 학습 활동

0) { ?>
  • -

아직 학습 활동이 없습니다.

강의 둘러보기

#9. 프론트엔드 디자인

9.1 반응형 CSS 구현

동영상 강의 사이트를 위한 CSS 스타일을 정의합니다.

/* mobile/lecture/lecture.css */

/* 공통 스타일 */
.lecture-container {
    padding: 1rem;
}

.section-title {
    font-size: 1.5rem;
    font-weight: bold;
    margin-bottom: 1rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid #eee;
}

/* 강의 목록 페이지 */
.lecture-list {
    display: grid;
    grid-template-columns: repeat(1, 1fr);
    gap: 1rem;
}

@media (min-width: 768px) {
    .lecture-list {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 992px) {
    .lecture-list {
        grid-template-columns: repeat(3, 1fr);
    }
}

.lecture-card {
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s ease;
    background-color: #fff;
}

.lecture-card:hover {
    transform: translateY(-5px);
}

.lecture-thumb {
    position: relative;
    width: 100%;
    padding-top: 56.25%; /* 16:9 비율 */
    overflow: hidden;
}

.lecture-thumb img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.lecture-info {
    padding: 1rem;
}

.lecture-title {
    font-size: 1.2rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
    line-height: 1.4;
    height: 2.8em;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.lecture-meta {
    display: flex;
    justify-content: space-between;
    margin-bottom: 0.5rem;
    color: #666;
    font-size: 0.9rem;
}

.lecture-price {
    font-weight: bold;
    color: #333;
    font-size: 1.1rem;
}

.lecture-discount {
    color: #e53e3e;
    margin-right: 0.5rem;
}

.lecture-original-price {
    text-decoration: line-through;
    color: #999;
    font-size: 0.9rem;
}

/* 동영상 플레이어 */
.video-player-container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 1rem;
}

.video-title {
    font-size: 1.5rem;
    font-weight: bold;
    margin-bottom: 1rem;
}

.video-player {
    position: relative;
    width: 100%;
    padding-top: 56.25%; /* 16:9 비율 */
    background-color: #000;
    margin-bottom: 1rem;
}

.video-player video,
.video-player iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.video-info {
    background-color: #f8f9fa;
    padding: 1rem;
    border-radius: 8px;
    margin-bottom: 1rem;
}

.video-description {
    margin-bottom: 1rem;
    line-height: 1.6;
}

.video-progress {
    margin-top: 1rem;
}

.progress {
    height: 8px;
    background-color: #e9ecef;
    border-radius: 4px;
    overflow: hidden;
    margin-bottom: 0.5rem;
}

.progress-bar {
    height: 100%;
    background-color: #4299e1;
    transition: width 0.3s ease;
}

.progress-text {
    display: flex;
    justify-content: space-between;
    font-size: 0.9rem;
    color: #666;
}

/* 내 강의실 */
.my-course-container {
    padding: 1rem;
    max-width: 1000px;
    margin: 0 auto;
}

.course-title {
    font-size: 1.8rem;
    font-weight: bold;
    margin-bottom: 1.5rem;
    text-align: center;
}

.course-item {
    display: flex;
    margin-bottom: 1.5rem;
    background-color: #fff;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.course-thumb {
    width: 120px;
    height: 80px;
    overflow: hidden;
}

.course-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.course-info {
    flex: 1;
    padding: 1rem;
}

.course-name {
    font-size: 1.2rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
}

.progress-container {
    margin-bottom: 0.5rem;
}

.progress-text {
    font-size: 0.9rem;
    color: #666;
}

.last-learning {
    font-size: 0.9rem;
    color: #666;
    margin-bottom: 0.5rem;
}

.last-date {
    color: #999;
    font-size: 0.8rem;
}

.course-action {
    margin-top: 1rem;
}

.empty-course {
    text-align: center;
    padding: 2rem;
    background-color: #f8f9fa;
    border-radius: 8px;
}

.empty-course p {
    margin-bottom: 1rem;
    color: #666;
}

/* 결제 페이지 */
.payment-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 1rem;
}

.payment-title {
    font-size: 1.8rem;
    font-weight: bold;
    margin-bottom: 1.5rem;
    text-align: center;
}

.lecture-info {
    display: flex;
    margin-bottom: 1.5rem;
    background-color: #f8f9fa;
    border-radius: 8px;
    padding: 1rem;
}

.lecture-detail {
    flex: 1;
    padding-left: 1rem;
}

.price-info {
    margin-top: 1rem;
}

.original-price {
    text-decoration: line-through;
    color: #999;
}

.discount-rate {
    color: #e53e3e;
    margin-bottom: 0.5rem;
}

.final-price {
    font-size: 1.5rem;
    font-weight: bold;
    color: #333;
}

.payment-method {
    margin-bottom: 1.5rem;
}

.method-options {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
}

.method-item {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    display: flex;
    align-items: center;
    cursor: pointer;
}

.method-item input {
    margin-right: 0.5rem;
}

.payment-agreement {
    margin-bottom: 1.5rem;
}

.agreement-check {
    display: flex;
    align-items: center;
    margin-bottom: 0.5rem;
}

.agreement-check input {
    margin-right: 0.5rem;
}

.agreement-detail {
    padding: 1rem;
    background-color: #f8f9fa;
    border-radius: 4px;
    font-size: 0.9rem;
    color: #666;
}

.payment-action {
    margin-top: 2rem;
}

9.2 기본 테마 수정

그누보드5 기본 테마를 취미 강의 사이트에 맞게 커스터마이징하는 방법입니다.

CSS 및 JavaScript 파일 추가

강의 사이트에 필요한 CSS와 JavaScript 파일을 추가합니다. 아래 코드를 head.php 파일 하단에 추가합니다.

<?php if (defined('_LECTURE_')) { ?>
<link rel="stylesheet" href="<?php echo G5_MOBILE_URL; ?>/lecture/lecture.css">
<script src="<?php echo G5_MOBILE_URL; ?>/lecture/lecture.js"></script>
<?php } ?>

각 강의 페이지에서는 _LECTURE_ 상수를 선언하여 사용합니다:

// mobile/lecture/list.php 등의 페이지 상단
define('_LECTURE_', true); // 강의 페이지임을 명시
include_once('./_common.php');

#10. 성능 최적화

10.1 동영상 성능 최적화

대용량 동영상 컨텐츠의 성능을 최적화하는 방법입니다.

동영상 최적화 방법

  1. 외부 스트리밍 서비스 활용

    Vimeo, YouTube, 네이버 클라우드 플랫폼과 같은 전문 동영상 호스팅 서비스를 활용하여 트래픽 부하를 줄입니다.

  2. 적응형 스트리밍 구현

    HLS(HTTP Live Streaming) 또는 DASH(Dynamic Adaptive Streaming over HTTP) 기술을 사용하여 사용자의 네트워크 환경에 맞게 동영상 품질을 자동 조절합니다.

    // HLS 형식의 동영상 재생 예제
    <video id="lecture-video" controls playsinline>
        <source src="https://example.com/videos/lecture.m3u8" type="application/x-mpegURL">
        브라우저가 HLS를 지원하지 않습니다.
    </video>
    
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        const video = document.getElementById('lecture-video');
        const videoSrc = 'https://example.com/videos/lecture.m3u8';
        
        if (Hls.isSupported()) {
            const hls = new Hls();
            hls.loadSource(videoSrc);
            hls.attachMedia(video);
        } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
            video.src = videoSrc;
        }
    });
    </script>
  3. 동영상 프리로딩 전략

    사용자가 시청할 가능성이 높은 다음 동영상을 미리 로드하여 끊김 없는 학습 경험을 제공합니다.

  4. CDN(Content Delivery Network) 활용

    동영상 파일을 CDN에 배포하여 지리적으로 가까운 서버에서 컨텐츠를 제공함으로써 로딩 속도를 향상시킵니다.

10.2 데이터베이스 최적화

다수의 사용자가 동시에 강의를 시청할 때 발생할 수 있는 데이터베이스 부하를 최적화하는 방법입니다.

-- 진도 저장을 위한 인덱스 추가
ALTER TABLE g5_lecture_progress ADD INDEX idx_mb_id_video (mb_id, video_id);
ALTER TABLE g5_lecture_progress ADD INDEX idx_mb_id_lt_id (mb_id, lt_id);

-- 결제 정보 조회를 위한 인덱스 추가
ALTER TABLE g5_lecture_purchase ADD INDEX idx_mb_id_status (mb_id, purchase_status);
ALTER TABLE g5_lecture_purchase ADD INDEX idx_lt_id_status (lt_id, purchase_status);

-- 강의 영상 조회를 위한 인덱스
ALTER TABLE g5_lecture_video ADD INDEX idx_lt_id_order (lt_id, video_order);

-- 주기적인 데이터 정리를 위한 스케줄링 작업
CREATE EVENT clean_old_progress_logs
ON SCHEDULE EVERY 1 WEEK
DO
  DELETE FROM g5_lecture_activity_log WHERE log_datetime < DATE_SUB(NOW(), INTERVAL 3 MONTH);

진도 저장 최적화

사용자가 비디오를 시청하는 동안 5초마다 진도를 저장하는 대신, 다음과 같은 전략을 고려할 수 있습니다:

  • 주요 시청 이벤트(재생, 일시정지, 종료)에서만 진도 저장
  • 일정 비율(예: 10%)마다 진도 저장
  • 일괄 처리를 위해 로컬에 진도 정보를 임시 저장 후 주기적으로 서버에 전송

10.3 캐싱 전략

그누보드5 기반 취미 강의 사이트의 성능을 향상시키기 위한 캐싱 전략입니다.

// config.php 파일에 캐싱 설정 추가
define('G5_USE_CACHE', true);
define('G5_CACHE_DIR', G5_DATA_PATH.'/cache');

// 강의 목록 캐싱 함수 예제
function get_cached_lecture_list($ca_id, $limit = 10, $cache_time = 3600) {
    global $g5;
    
    if (!G5_USE_CACHE) {
        return get_lecture_list($ca_id, $limit);
    }
    
    $cache_file = G5_CACHE_DIR.'/lecture_list_'.$ca_id.'_'.$limit.'.php';
    
    // 캐시 파일이 있고, 유효기간이 지나지 않았으면 캐시 사용
    if (file_exists($cache_file) && time() - filemtime($cache_file) < $cache_time) {
        return include($cache_file);
    }
    
    // 캐시가 없거나 만료되었으면 DB 조회
    $lecture_list = get_lecture_list($ca_id, $limit);
    
    // 캐시 디렉토리 확인 및 생성
    if (!is_dir(G5_CACHE_DIR)) {
        @mkdir(G5_CACHE_DIR, G5_DIR_PERMISSION);
        @chmod(G5_CACHE_DIR, G5_DIR_PERMISSION);
    }
    
    // 캐시 파일 저장
    $cache_content = "";
    file_put_contents($cache_file, $cache_content);
    
    return $lecture_list;
}

// 브라우저 캐싱을 위한 헤더 설정
function set_lecture_cache_headers($max_age = 3600) {
    header('Cache-Control: public, max-age='.$max_age);
    header('Expires: '.gmdate('D, d M Y H:i:s', time() + $max_age).' GMT');
}

#11. 결론

그누보드5 기반 취미 강의 사이트 구현 요약

이 가이드에서는 그누보드5를 기반으로 취미 강의 사이트를 구현하는 방법을 다루었습니다. 핵심 구현 요소는 다음과 같습니다:

  • 강의 및 동영상 관리를 위한 데이터베이스 구조 설계
  • 동영상 업로드 및 재생 시스템 구현
  • 학습 진도 추적 기능 개발
  • 결제 시스템 연동 방법
  • 회원 관리 및 학습 통계 기능 구현
  • 모바일 및 데스크톱 환경을 고려한 반응형 디자인
  • 대용량 동영상 처리를 위한 성능 최적화 방안

이러한 요소들을 결합하여 그누보드5 기반의 완성도 높은 취미 강의 사이트를 구축할 수 있습니다. 사용자의 필요에 맞게 기능을 확장하고 커스터마이징하여 더 풍부한 학습 경험을 제공할 수 있습니다.

추가 개발 가능 기능

학습 커뮤니티 기능

  • 강의별 Q&A 게시판
  • 학습 노트 및 메모 기능
  • 수강생 간 소통 채널

강사 관리 시스템

  • 강사별 대시보드
  • 수익 정산 시스템
  • 강의 통계 및 피드백

학습 게임화 요소

  • 학습 배지 및 성취 시스템
  • 수강생 랭킹 시스템
  • 챌린지 및 미션 기능

AI 기반 학습 추천

  • 개인화된 강의 추천
  • 학습 패턴 분석 및 피드백
  • 맞춤형 학습 경로 제안