/* ============================================================================================================= */ /* 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 ----------------------------------------------------
cases.php ----------------------------------------------------
*/ 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); } }); });