/* ============================================================================================================= */
/* AI 영상 제작사례 YouTube Modal - INDEX + CASES SUBPAGE 통합 */
/* 적용 페이지 */
/* 1. INDEX : Swiper 제작사례 */
/* 2. cases.php : 제작사례 일반 리스트 */
/* 주요 기능 */
/* - INDEX / cases.php 동일 JS 사용 */
/* - Swiper loop 복제 슬라이드 대응 */
/* - data-youtube에 YouTube URL 또는 영상 ID 사용 가능 */
/* - 모달 오픈 시 자동재생하지 않음 */
/* - 이전 / 다음 영상 이동 */
/* - 영상 변경 시 기존 iframe src 제거 → 재생 및 소리 즉시 정지 */
/* - 마지막 영상 → 다음 클릭 시 첫 번째 영상으로 이동 */
/* - 첫 번째 영상 → 이전 클릭 시 마지막 영상으로 이동 */
/* - ESC : 모달 닫기 */
/* - 키보드 ← / → : 이전 / 다음 영상 */
/* ============================================================================================================= */
document.addEventListener('DOMContentLoaded', function () {
/* ========================================================================================================= */
/* 01. 모달 관련 요소 */
/* ========================================================================================================= */
const modal = document.getElementById('caseYoutubeModal');
const iframe = document.getElementById('caseYoutubeIframe');
const title = document.getElementById('caseYoutubeTitle');
const prevButton = document.getElementById('caseYoutubePrev');
const nextButton = document.getElementById('caseYoutubeNext');
const currentText = document.getElementById('caseYoutubeCurrent');
const totalText = document.getElementById('caseYoutubeTotal');
/*
모달 또는 iframe이 페이지에 없는 경우
해당 페이지에서는 스크립트를 실행하지 않습니다.
*/
if (!modal || !iframe) return;
/* ========================================================================================================= */
/* 02. 제작사례 영상 목록 가져오기 */
/* ========================================================================================================= */
/*
INDEX와 cases.php 양쪽에서 사용할 수 있도록
특정 Swiper 클래스에 종속시키지 않습니다.
아래 조건을 가지고 있는 버튼을 모두 찾습니다.
.media-thumb
data-youtube
data-case-index
INDEX
----------------------------------------------------
*/
let sourceButtons = Array.from(
document.querySelectorAll(
'.media-thumb[data-youtube][data-case-index]'
)
);
/* ========================================================================================================= */
/* 03. Swiper Loop 복제 슬라이드 제거 */
/* ========================================================================================================= */
/*
INDEX에서 Swiper loop를 사용할 경우
Swiper가 동일 슬라이드를 복제할 수 있습니다.
복제된 슬라이드까지 영상 목록에 포함시키면
01 / 07
이어야 하는 것이
01 / 14
처럼 중복될 수 있으므로 제거합니다.
cases.php에는 swiper-slide-duplicate가 없으므로
아무 영향이 없습니다.
*/
sourceButtons = sourceButtons.filter(function (button) {
return !button.closest('.swiper-slide-duplicate');
});
/* ========================================================================================================= */
/* 04. 동일한 data-case-index 중복 제거 */
/* ========================================================================================================= */
/*
Swiper 버전이나 loop 처리 방식에 따라
복제 요소가 별도의 duplicate 클래스를 가지지 않는 경우도 대비합니다.
같은 data-case-index가 여러 개라면
첫 번째 요소만 제작사례 목록으로 사용합니다.
*/
sourceButtons = sourceButtons.filter(function (button, index, array) {
return array.findIndex(function (item) {
return item.dataset.caseIndex === button.dataset.caseIndex;
}) === index;
});
/* ========================================================================================================= */
/* 05. data-case-index 순서대로 영상 정렬 */
/* ========================================================================================================= */
/*
HTML 순서가 달라지더라도
data-case-index="0"
data-case-index="1"
data-case-index="2"
순서대로 이전 / 다음 영상이 작동하도록 정렬합니다.
*/
sourceButtons.sort(function (a, b) {
return Number(a.dataset.caseIndex) - Number(b.dataset.caseIndex);
});
/* 현재 영상의 배열 위치 */
let currentIndex = 0;
/*
이전 / 다음 영상으로 변경할 때 사용하는 타이머
*/
let switchingTimer = null;
/* ========================================================================================================= */
/* 06. 전체 영상 개수 표시 */
/* ========================================================================================================= */
if (totalText) {
totalText.textContent = String(sourceButtons.length).padStart(2, '0');
}
/* ========================================================================================================= */
/* 07. YouTube URL → Video ID 추출 */
/* ========================================================================================================= */
/*
다음 형태를 모두 사용할 수 있습니다.
① 일반 YouTube 주소
data-youtube="https://www.youtube.com/watch?v=M7lc1UVf-VE"
② 짧은 주소
data-youtube="https://youtu.be/M7lc1UVf-VE"
③ Embed 주소
data-youtube="https://www.youtube.com/embed/M7lc1UVf-VE"
④ Shorts 주소
data-youtube="https://www.youtube.com/shorts/M7lc1UVf-VE"
⑤ Live 주소
data-youtube="https://www.youtube.com/live/M7lc1UVf-VE"
⑥ Video ID만 입력
data-youtube="M7lc1UVf-VE"
*/
function getYoutubeVideoId(value) {
/* 값이 없으면 종료 */
if (!value) return '';
const input = String(value).trim();
/* -----------------------------------------------------
영상 ID만 입력된 경우
YouTube 영상 ID는 일반적으로 11자리입니다.
----------------------------------------------------- */
if (/^[a-zA-Z0-9_-]{11}$/.test(input)) {
return input;
}
/* -----------------------------------------------------
URL 형태인 경우
----------------------------------------------------- */
try {
const url = new URL(input, window.location.href);
const host = url.hostname.replace(/^www\./, '');
/* -------------------------------------------------
youtu.be/VIDEO_ID
------------------------------------------------- */
if (host === 'youtu.be') {
return url.pathname
.split('/')
.filter(Boolean)[0] || '';
}
/* -------------------------------------------------
youtube.com
------------------------------------------------- */
if (
host === 'youtube.com' ||
host.endsWith('.youtube.com')
) {
/*
watch?v=VIDEO_ID
*/
const watchId = url.searchParams.get('v');
if (watchId) {
return watchId;
}
/*
/embed/VIDEO_ID
/shorts/VIDEO_ID
/live/VIDEO_ID
*/
const path = url.pathname
.split('/')
.filter(Boolean);
if (
path[0] === 'embed' ||
path[0] === 'shorts' ||
path[0] === 'live'
) {
return path[1] || '';
}
}
} catch (error) {
console.warn(
'YouTube 주소 형식을 확인해주세요.',
input
);
return '';
}
return '';
}
/* ========================================================================================================= */
/* 08. YouTube Embed URL 생성 */
/* ========================================================================================================= */
function makeEmbedUrl(videoId) {
/*
autoplay=0
모달이 열려도 영상을 자동재생하지 않습니다.
사용자가 YouTube 재생 버튼을 눌러야 재생됩니다.
*/
return 'https://www.youtube.com/embed/' + videoId
+ '?autoplay=0'
/* 관련 영상 노출 최소화 */
+ '&rel=0'
/* 모바일 브라우저에서 페이지 안에서 재생 */
+ '&playsinline=1'
/* YouTube UI 최소화 */
+ '&modestbranding=1'
/* YouTube Player API 사용 가능 */
+ '&enablejsapi=1';
}
/* ========================================================================================================= */
/* 09. 현재 재생 중인 YouTube 영상 정지 */
/* ========================================================================================================= */
function stopCurrentVideo() {
/*
기존에 예약되어 있는 영상 변경 타이머가 있다면 제거
*/
if (switchingTimer) {
window.clearTimeout(switchingTimer);
switchingTimer = null;
}
/*
iframe의 src를 제거하면
현재 YouTube 영상과 소리가 즉시 정지됩니다.
모달을 닫았는데도
영상 소리가 계속 나오는 문제를 방지합니다.
*/
iframe.src = '';
}
/* ========================================================================================================= */
/* 10. 현재 영상 번호 표시 */
/* ========================================================================================================= */
function updateCounter() {
if (currentText) {
currentText.textContent =
String(currentIndex + 1).padStart(2, '0');
}
}
/* ========================================================================================================= */
/* 11. YouTube 영상 불러오기 */
/* ========================================================================================================= */
function loadVideo(index, options) {
/*
등록된 제작사례가 하나도 없다면 종료
*/
if (!sourceButtons.length) return false;
const config = options || {};
/*
index 값이 배열 범위를 넘어가도
다시 처음 / 마지막으로 연결합니다.
예)
영상 5개일 경우
마지막 05에서 NEXT
→ 01
첫 번째 01에서 PREV
→ 05
*/
const normalizedIndex =
(index + sourceButtons.length) % sourceButtons.length;
const button = sourceButtons[normalizedIndex];
/*
현재 제작사례의 data-youtube에서
YouTube Video ID를 추출합니다.
*/
const videoId = getYoutubeVideoId(
button.dataset.youtube
);
/*
YouTube ID를 가져오지 못한 경우
빈 모달을 띄우지 않고 종료합니다.
*/
if (!videoId) {
console.warn(
'YouTube 영상 ID를 확인할 수 없습니다:',
button.dataset.youtube
);
return false;
}
/* 현재 영상 위치 저장 */
currentIndex = normalizedIndex;
/* 현재 영상 번호 변경 */
updateCounter();
/* -----------------------------------------------------
영상 타이틀 변경
----------------------------------------------------- */
const videoTitle =
button.dataset.title ||
'AI 영상 제작사례';
if (title) {
title.textContent = videoTitle;
}
iframe.title = videoTitle;
/* -----------------------------------------------------
이전 / 다음 영상으로 이동한 경우
----------------------------------------------------- */
if (config.stopBeforeLoad) {
/*
먼저 기존 iframe을 정지시킵니다.
*/
stopCurrentVideo();
/*
브라우저가 기존 YouTube Player를
완전히 종료할 수 있도록 아주 짧은 시간 후
다음 영상을 불러옵니다.
*/
switchingTimer = window.setTimeout(function () {
iframe.src = makeEmbedUrl(videoId);
switchingTimer = null;
}, 60);
} else {
/*
처음 모달을 열었을 때
선택한 영상을 iframe에 삽입합니다.
*/
iframe.src = makeEmbedUrl(videoId);
}
return true;
}
/* ========================================================================================================= */
/* 12. 제작사례 모달 열기 */
/* ========================================================================================================= */
function openYoutubeModal(button) {
/*
중요:
data-case-index의 숫자를 그대로 배열 index로
사용하지 않습니다.
예를 들어
data-case-index="10"
이어도 실제 배열에서는 세 번째 영상일 수 있기 때문입니다.
현재 클릭한 data-case-index와 일치하는
실제 배열 위치를 찾아 사용합니다.
*/
const caseIndex = String(
button.dataset.caseIndex
);
const index = sourceButtons.findIndex(function (item) {
return String(item.dataset.caseIndex) === caseIndex;
});
/*
영상 목록에서 찾지 못하면 종료
*/
if (index < 0) return;
/*
YouTube 영상이 정상적인 경우에만
모달을 열도록 합니다.
*/
const loaded = loadVideo(
index,
{
stopBeforeLoad: false
}
);
if (!loaded) return;
/* 모달 활성화 */
modal.classList.add('on');
/* 접근성 상태 변경 */
modal.setAttribute(
'aria-hidden',
'false'
);
/*
body에 클래스 추가
CSS에서
body.youtube-modal-open {
overflow:hidden;
}
형태로 사용하면 모달 뒤 페이지 스크롤을
막을 수 있습니다.
*/
document.body.classList.add(
'youtube-modal-open'
);
}
/* ========================================================================================================= */
/* 13. 제작사례 모달 닫기 */
/* ========================================================================================================= */
function closeYoutubeModal() {
/*
모달 닫기 전에 반드시
현재 YouTube 영상부터 정지합니다.
*/
stopCurrentVideo();
/* 모달 비활성 */
modal.classList.remove('on');
/* 접근성 상태 변경 */
modal.setAttribute(
'aria-hidden',
'true'
);
/* body 스크롤 상태 복원 */
document.body.classList.remove(
'youtube-modal-open'
);
}
/* ========================================================================================================= */
/* 14. 이전 / 다음 영상 */
/* ========================================================================================================= */
function moveVideo(direction) {
if (!sourceButtons.length) return;
/*
direction
-1 = 이전
1 = 다음
*/
loadVideo(
currentIndex + direction,
{
stopBeforeLoad: true
}
);
}
/* ========================================================================================================= */
/* 15. 제작사례 썸네일 클릭 */
/* ========================================================================================================= */
/*
document에 이벤트를 등록하는 이벤트 위임 방식입니다.
이 방식을 사용하면 INDEX에서 Swiper가
슬라이드 DOM을 처리하거나 복제해도
클릭 이벤트가 정상 작동합니다.
*/
document.addEventListener('click', function (event) {
const button = event.target.closest(
'.media-thumb[data-youtube][data-case-index]'
);
/* 제작사례 버튼이 아니면 종료 */
if (!button) return;
event.preventDefault();
/* YouTube 모달 오픈 */
openYoutubeModal(button);
});
/* ========================================================================================================= */
/* 16. 이전 영상 버튼 */
/* ========================================================================================================= */
if (prevButton) {
prevButton.addEventListener('click', function () {
moveVideo(-1);
});
}
/* ========================================================================================================= */
/* 17. 다음 영상 버튼 */
/* ========================================================================================================= */
if (nextButton) {
nextButton.addEventListener('click', function () {
moveVideo(1);
});
}
/* ========================================================================================================= */
/* 18. 모달 닫기 버튼 / DIM 클릭 */
/* ========================================================================================================= */
modal
.querySelectorAll('[data-youtube-close]')
.forEach(function (closeButton) {
closeButton.addEventListener(
'click',
closeYoutubeModal
);
});
/* ========================================================================================================= */
/* 19. 키보드 제어 */
/* ========================================================================================================= */
document.addEventListener('keydown', function (event) {
/*
모달이 열려있지 않다면
키보드 이벤트 처리하지 않음
*/
if (!modal.classList.contains('on')) return;
/* ESC → 모달 닫기 */
if (event.key === 'Escape') {
closeYoutubeModal();
/* ← → 이전 영상 */
} else if (event.key === 'ArrowLeft') {
moveVideo(-1);
/* → → 다음 영상 */
} else if (event.key === 'ArrowRight') {
moveVideo(1);
}
});
});