Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+409
View File
@@ -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 };
}