Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* 비디오 관련 기본 클래스
|
||||
* @module VideoBase
|
||||
*/
|
||||
class VideoBase {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
autoplay: false,
|
||||
controls: true,
|
||||
loop: false,
|
||||
muted: false,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube 비디오 URL 생성
|
||||
* @param {string} videoId - YouTube 비디오 ID
|
||||
* @param {Object} options - 추가 옵션
|
||||
* @returns {string}
|
||||
*/
|
||||
static getYouTubeUrl(videoId, options = {}) {
|
||||
const {
|
||||
autoplay = 0,
|
||||
controls = 1,
|
||||
loop = 0,
|
||||
muted = 0,
|
||||
rel = 0,
|
||||
modestbranding = 1,
|
||||
start = 0,
|
||||
} = options;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
autoplay,
|
||||
controls,
|
||||
loop,
|
||||
muted,
|
||||
rel,
|
||||
modestbranding,
|
||||
...(start > 0 && { start }),
|
||||
});
|
||||
|
||||
return `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube 썸네일 URL 생성
|
||||
* @param {string} videoId - YouTube 비디오 ID
|
||||
* @param {string} quality - 품질 (default, hq, mq, sd, maxres)
|
||||
* @returns {string}
|
||||
*/
|
||||
static getYouTubeThumbnail(videoId, quality = "sddefault") {
|
||||
const qualities = {
|
||||
default: "default.jpg",
|
||||
hq: "hqdefault.jpg",
|
||||
mq: "mqdefault.jpg",
|
||||
sd: "sddefault.jpg",
|
||||
maxres: "maxresdefault.jpg",
|
||||
};
|
||||
|
||||
const thumbnailFile = qualities[quality] || qualities.sd;
|
||||
return `https://img.youtube.com/vi/${videoId}/${thumbnailFile}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 ID 추출 (YouTube URL에서)
|
||||
* @param {string} url - YouTube URL
|
||||
* @returns {string|null}
|
||||
*/
|
||||
static extractYouTubeId(url) {
|
||||
const patterns = [
|
||||
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
|
||||
/youtube\.com\/v\/([^&\n?#]+)/,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = url.match(pattern);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 iframe 생성
|
||||
* @param {string} videoId - 비디오 ID
|
||||
* @param {Object} options - iframe 옵션
|
||||
* @returns {HTMLIFrameElement}
|
||||
*/
|
||||
static createIframe(videoId, options = {}) {
|
||||
const {
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
autoplay = 0,
|
||||
controls = 1,
|
||||
className = "",
|
||||
id = "",
|
||||
} = options;
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.width = width;
|
||||
iframe.height = height;
|
||||
iframe.src = this.getYouTubeUrl(videoId, { autoplay, controls });
|
||||
iframe.frameBorder = "0";
|
||||
iframe.allow =
|
||||
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
|
||||
iframe.allowFullscreen = true;
|
||||
|
||||
if (className) iframe.className = className;
|
||||
if (id) iframe.id = id;
|
||||
|
||||
return iframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 재생
|
||||
* @param {HTMLIFrameElement} iframe - iframe 요소
|
||||
*/
|
||||
static play(iframe) {
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
iframe.contentWindow.postMessage(
|
||||
'{"event":"command","func":"playVideo","args":""}',
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 일시정지
|
||||
* @param {HTMLIFrameElement} iframe - iframe 요소
|
||||
*/
|
||||
static pause(iframe) {
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
iframe.contentWindow.postMessage(
|
||||
'{"event":"command","func":"pauseVideo","args":""}',
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 정지
|
||||
* @param {HTMLIFrameElement} iframe - iframe 요소
|
||||
*/
|
||||
static stop(iframe) {
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
iframe.contentWindow.postMessage(
|
||||
'{"event":"command","func":"stopVideo","args":""}',
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 시간 이동
|
||||
* @param {HTMLIFrameElement} iframe - iframe 요소
|
||||
* @param {number} seconds - 이동할 시간 (초)
|
||||
*/
|
||||
static seekTo(iframe, seconds) {
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
iframe.contentWindow.postMessage(
|
||||
`{"event":"command","func":"seekTo","args":[${seconds}, true]}`,
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 볼륨 설정
|
||||
* @param {HTMLIFrameElement} iframe - iframe 요소
|
||||
* @param {number} volume - 볼륨 (0-100)
|
||||
*/
|
||||
static setVolume(iframe, volume) {
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
const vol = Math.max(0, Math.min(100, volume));
|
||||
iframe.contentWindow.postMessage(
|
||||
`{"event":"command","func":"setVolume","args":[${vol}]}`,
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 음소거 토글
|
||||
* @param {HTMLIFrameElement} iframe - iframe 요소
|
||||
* @param {boolean} mute - 음소거 여부
|
||||
*/
|
||||
static toggleMute(iframe, mute) {
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
const func = mute ? "mute" : "unMute";
|
||||
iframe.contentWindow.postMessage(
|
||||
`{"event":"command","func":"${func}","args":""}`,
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 데이터 모델
|
||||
*/
|
||||
class VideoModel {
|
||||
constructor(data = {}) {
|
||||
this.id = data.id || null;
|
||||
this.url = data.url || "";
|
||||
this.title = data.title || "";
|
||||
this.category = data.category || "";
|
||||
this.subcate = data.subcate || "";
|
||||
this.keywords = data.keywords || [];
|
||||
this.bookmark = data.bookmark || false;
|
||||
this.completed = data.completed || false;
|
||||
this.picker = data.picker || "";
|
||||
this.type = data.type || "main";
|
||||
this.gauge = data.gauge || 0;
|
||||
this.description = data.description || "";
|
||||
this.duration = data.duration || 0;
|
||||
this.createdAt = data.createdAt || new Date();
|
||||
this.updatedAt = data.updatedAt || new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 ID 가져오기
|
||||
* @returns {string}
|
||||
*/
|
||||
getVideoId() {
|
||||
return VideoBase.extractYouTubeId(this.url) || this.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 썸네일 URL 가져오기
|
||||
* @param {string} quality - 품질
|
||||
* @returns {string}
|
||||
*/
|
||||
getThumbnailUrl(quality = "sd") {
|
||||
const videoId = this.getVideoId();
|
||||
return VideoBase.getYouTubeThumbnail(videoId, quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* 임베드 URL 가져오기
|
||||
* @param {Object} options - 옵션
|
||||
* @returns {string}
|
||||
*/
|
||||
getEmbedUrl(options = {}) {
|
||||
const videoId = this.getVideoId();
|
||||
return VideoBase.getYouTubeUrl(videoId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 토글
|
||||
*/
|
||||
toggleBookmark() {
|
||||
this.bookmark = !this.bookmark;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료 상태 설정
|
||||
* @param {boolean} completed - 완료 여부
|
||||
*/
|
||||
setCompleted(completed) {
|
||||
this.completed = completed;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON으로 변환
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
url: this.url,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
subcate: this.subcate,
|
||||
keywords: this.keywords,
|
||||
bookmark: this.bookmark,
|
||||
completed: this.completed,
|
||||
picker: this.picker,
|
||||
type: this.type,
|
||||
gauge: this.gauge,
|
||||
description: this.description,
|
||||
duration: this.duration,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 컬렉션 관리
|
||||
*/
|
||||
class VideoCollection {
|
||||
constructor(videos = []) {
|
||||
this.videos = videos.map((v) => (v instanceof VideoModel ? v : new VideoModel(v)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 추가
|
||||
* @param {Object|VideoModel} video - 비디오 데이터
|
||||
*/
|
||||
add(video) {
|
||||
const model = video instanceof VideoModel ? video : new VideoModel(video);
|
||||
this.videos.push(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 제거
|
||||
* @param {number|string} id - 비디오 ID
|
||||
*/
|
||||
remove(id) {
|
||||
this.videos = this.videos.filter((v) => v.id !== id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 비디오 찾기
|
||||
* @param {number|string} id - 비디오 ID
|
||||
* @returns {VideoModel|null}
|
||||
*/
|
||||
findById(id) {
|
||||
return this.videos.find((v) => v.id === id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 필터링
|
||||
* @param {Function} predicate - 필터 함수
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
filter(predicate) {
|
||||
return new VideoCollection(this.videos.filter(predicate));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리로 필터링
|
||||
* @param {string} category - 카테고리
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
filterByCategory(category) {
|
||||
return this.filter((v) => v.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 키워드로 필터링
|
||||
* @param {Array} keywords - 키워드 배열
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
filterByKeywords(keywords) {
|
||||
return this.filter((v) => keywords.some((k) => v.keywords.includes(k)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크된 비디오만
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
getBookmarked() {
|
||||
return this.filter((v) => v.bookmark);
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료된 비디오만
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
getCompleted() {
|
||||
return this.filter((v) => v.completed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 미완료 비디오만
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
getIncomplete() {
|
||||
return this.filter((v) => !v.completed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 정렬
|
||||
* @param {Function} compareFn - 비교 함수
|
||||
* @returns {VideoCollection}
|
||||
*/
|
||||
sort(compareFn) {
|
||||
return new VideoCollection([...this.videos].sort(compareFn));
|
||||
}
|
||||
|
||||
/**
|
||||
* 개수
|
||||
* @returns {number}
|
||||
*/
|
||||
count() {
|
||||
return this.videos.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열로 변환
|
||||
* @returns {Array}
|
||||
*/
|
||||
toArray() {
|
||||
return this.videos;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON으로 변환
|
||||
* @returns {Array}
|
||||
*/
|
||||
toJSON() {
|
||||
return this.videos.map((v) => v.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
// ES6 모듈 내보내기
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { VideoBase, VideoModel, VideoCollection };
|
||||
}
|
||||
Reference in New Issue
Block a user