// ======================================== // Portfolio 페이지 전용 JavaScript // ======================================== document.addEventListener('DOMContentLoaded', function() { initPortfolioTabs(); initVideoLazyLoading(); initPortfolioAnimations(); initVideoInteractions(); initYouTubeAPI(); }); // ======================================== // 포트폴리오 탭 시스템 // ======================================== function initPortfolioTabs() { const tabButtons = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); // 탭 버튼 클릭 이벤트 tabButtons.forEach(button => { button.addEventListener('click', () => { const targetTab = button.getAttribute('data-tab'); // 모든 탭 버튼에서 active 클래스 제거 tabButtons.forEach(btn => btn.classList.remove('active')); // 클릭된 탭 버튼에 active 클래스 추가 button.classList.add('active'); // 모든 탭 콘텐츠 숨기기 tabContents.forEach(content => { content.classList.remove('active'); }); // 선택된 탭 콘텐츠 표시 const activeContent = document.getElementById(targetTab); if (activeContent) { activeContent.classList.add('active'); // 탭 전환 시 비디오 레이지 로딩 재초기화 setTimeout(() => { initVideoLazyLoading(); }, 100); } }); }); // 키보드 접근성 tabButtons.forEach(button => { button.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); button.click(); } }); }); } // 비디오 레이지 로딩 초기화 function initVideoLazyLoading() { const videoWrappers = document.querySelectorAll('.video-wrapper'); // Intersection Observer를 사용한 레이지 로딩 const observerOptions = { threshold: 0.1, rootMargin: '50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadVideo(entry.target); observer.unobserve(entry.target); } }); }, observerOptions); videoWrappers.forEach(wrapper => { observer.observe(wrapper); // 로딩 상태 표시 if (!wrapper.querySelector('.video-loading')) { const loadingDiv = document.createElement('div'); loadingDiv.className = 'video-loading'; loadingDiv.textContent = '비디오 로딩 중...'; wrapper.appendChild(loadingDiv); } }); } // 비디오 로딩 function loadVideo(wrapper) { const iframe = wrapper.querySelector('iframe'); if (iframe && iframe.dataset.src) { // 실제 src 설정 iframe.src = iframe.dataset.src; // 로딩 완료 처리 iframe.addEventListener('load', function() { wrapper.classList.add('loaded'); const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.remove(); } }); // 에러 처리 iframe.addEventListener('error', function() { const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.textContent = '비디오를 로드할 수 없습니다'; loadingDiv.style.color = '#ef4444'; } }); } else { // 이미 src가 설정된 경우 wrapper.classList.add('loaded'); const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.remove(); } } } // 포트폴리오 애니메이션 초기화 function initPortfolioAnimations() { // 비디오 카드 애니메이션 const videoCards = document.querySelectorAll('.video-card'); const observerOptions = { threshold: 0.1, rootMargin: '30px 0px' }; const cardObserver = new IntersectionObserver((entries) => { entries.forEach((entry, index) => { if (entry.isIntersecting) { setTimeout(() => { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; }, index * 150); cardObserver.unobserve(entry.target); } }); }, observerOptions); videoCards.forEach(card => { card.style.opacity = '0'; card.style.transform = 'translateY(30px)'; card.style.transition = 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)'; cardObserver.observe(card); }); // 기술 특징 애니메이션 const techFeatures = document.querySelectorAll('.tech-feature'); techFeatures.forEach((feature, index) => { feature.style.opacity = '0'; feature.style.transform = 'translateY(30px)'; feature.style.transition = 'all 0.6s ease'; setTimeout(() => { feature.style.opacity = '1'; feature.style.transform = 'translateY(0)'; }, 200 * (index + 1)); }); // YouTube 채널 카드 애니메이션 const channelCard = document.querySelector('.youtube-channel-card'); if (channelCard) { channelCard.style.opacity = '0'; channelCard.style.transform = 'scale(0.95)'; channelCard.style.transition = 'all 0.8s ease'; setTimeout(() => { channelCard.style.opacity = '1'; channelCard.style.transform = 'scale(1)'; }, 300); } } // 비디오 상호작용 초기화 function initVideoInteractions() { const videoCards = document.querySelectorAll('.video-card'); videoCards.forEach(card => { const iframe = card.querySelector('iframe'); const videoInfo = card.querySelector('.video-info'); // 카드 호버 효과 card.addEventListener('mouseenter', function() { if (videoInfo) { videoInfo.style.transform = 'translateY(-5px)'; videoInfo.style.transition = 'transform 0.3s ease'; } }); card.addEventListener('mouseleave', function() { if (videoInfo) { videoInfo.style.transform = 'translateY(0)'; } }); // 비디오 클릭 시 전체화면 효과 if (iframe) { iframe.addEventListener('click', function() { this.style.transform = 'scale(1.02)'; setTimeout(() => { this.style.transform = 'scale(1)'; }, 200); }); } }); // 태그 클릭 기능 const tags = document.querySelectorAll('.tag'); tags.forEach(tag => { tag.addEventListener('click', function() { const tagName = this.textContent.toLowerCase(); filterByTag(tagName); }); // 호버 효과 tag.addEventListener('mouseenter', function() { this.style.transform = 'scale(1.1)'; this.style.cursor = 'pointer'; }); tag.addEventListener('mouseleave', function() { this.style.transform = 'scale(1)'; }); }); } // 태그별 필터링 function filterByTag(tagName) { const videoCards = document.querySelectorAll('.video-card'); videoCards.forEach(card => { const tags = card.querySelectorAll('.tag'); const hasTag = Array.from(tags).some(tag => tag.textContent.toLowerCase().includes(tagName) ); if (hasTag) { card.style.display = 'block'; card.style.animation = 'fadeIn 0.5s ease'; } else { card.style.opacity = '0.3'; card.style.animation = 'fadeOut 0.5s ease'; } }); // 3초 후 필터 해제 setTimeout(() => { videoCards.forEach(card => { card.style.display = 'block'; card.style.opacity = '1'; card.style.animation = 'fadeIn 0.5s ease'; }); }, 3000); } // YouTube API 초기화 function initYouTubeAPI() { // YouTube IFrame API 로드 if (!window.YT) { const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); } // API 준비 완료 시 실행 window.onYouTubeIframeAPIReady = function() { initYouTubePlayers(); }; } // YouTube 플레이어 초기화 function initYouTubePlayers() { const youtubeIframes = document.querySelectorAll('iframe[src*="youtube.com"]'); const players = []; youtubeIframes.forEach((iframe, index) => { const videoId = extractVideoId(iframe.src); if (videoId) { // 기존 iframe 교체 const playerDiv = document.createElement('div'); playerDiv.id = `youtube-player-${index}`; iframe.parentNode.replaceChild(playerDiv, iframe); // YouTube 플레이어 생성 const player = new YT.Player(playerDiv.id, { height: '100%', width: '100%', videoId: videoId, playerVars: { modestbranding: 1, rel: 0, showinfo: 0, controls: 1, autoplay: 0 }, events: { onReady: onPlayerReady, onStateChange: onPlayerStateChange, onError: onPlayerError } }); players.push(player); } }); return players; } // YouTube 비디오 ID 추출 function extractVideoId(url) { const match = url.match(/embed\/([^?&]+)/); return match ? match[1] : null; } // 플레이어 준비 완료 function onPlayerReady(event) { console.log('YouTube player ready'); // 플레이어 컨테이너에 로딩 완료 클래스 추가 const playerElement = event.target.getIframe(); const wrapper = playerElement.closest('.video-wrapper'); if (wrapper) { wrapper.classList.add('loaded'); const loadingDiv = wrapper.querySelector('.video-loading'); if (loadingDiv) { loadingDiv.remove(); } } } // 플레이어 상태 변경 function onPlayerStateChange(event) { const playerElement = event.target.getIframe(); const videoCard = playerElement.closest('.video-card'); if (event.data === YT.PlayerState.PLAYING) { // 재생 시작 시 다른 비디오 일시정지 pauseOtherVideos(event.target); // 재생 중 표시 if (videoCard) { videoCard.classList.add('playing'); } // 재생 추적 (선택사항) trackVideoPlay(event.target); } else if (event.data === YT.PlayerState.PAUSED || event.data === YT.PlayerState.ENDED) { // 일시정지/종료 시 if (videoCard) { videoCard.classList.remove('playing'); } } } // 플레이어 오류 처리 function onPlayerError(event) { console.error('YouTube player error:', event.data); const playerElement = event.target.getIframe(); const wrapper = playerElement.closest('.video-wrapper'); if (wrapper) { const errorDiv = document.createElement('div'); errorDiv.className = 'video-error'; errorDiv.innerHTML = '비디오를 로드할 수 없습니다
네트워크 연결을 확인해주세요'; errorDiv.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #ef4444; font-size: 1rem; z-index: 10; `; wrapper.appendChild(errorDiv); } } // 다른 비디오 일시정지 function pauseOtherVideos(currentPlayer) { if (window.youtubePlayers) { window.youtubePlayers.forEach(player => { if (player !== currentPlayer && player.pauseVideo) { player.pauseVideo(); } }); } } // 비디오 재생 추적 function trackVideoPlay(player) { if (typeof gtag !== 'undefined') { const videoTitle = player.getVideoData().title; gtag('event', 'video_play', { event_category: 'Portfolio', event_label: videoTitle, value: 1 }); } } // 비디오 공유 기능 function shareVideo(videoUrl, title) { if (navigator.share) { navigator.share({ title: title || '밍글 스튜디오 포트폴리오', url: videoUrl }); } else { // 클립보드에 복사 navigator.clipboard.writeText(videoUrl).then(() => { showNotification('비디오 링크가 클립보드에 복사되었습니다.', 'success'); }); } } // 포트폴리오 통계 function getPortfolioStats() { const longformVideos = document.querySelectorAll('.longform-grid .video-card').length; const shortsVideos = document.querySelectorAll('.shorts-grid .video-card').length; const broadcastVideos = document.querySelectorAll('.broadcast-grid .video-card').length; return { longform: longformVideos, shorts: shortsVideos, broadcasts: broadcastVideos, total: longformVideos + shortsVideos + broadcastVideos }; } // 알림 메시지 표시 function showNotification(message, type = 'info') { const existingNotification = document.querySelector('.notification'); if (existingNotification) { existingNotification.remove(); } const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.innerHTML = ` ${message} `; Object.assign(notification.style, { position: 'fixed', top: '20px', right: '20px', padding: '1rem 1.5rem', borderRadius: '8px', boxShadow: '0 4px 15px rgba(0, 0, 0, 0.15)', zIndex: '9999', maxWidth: '400px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '1rem' }); const colors = { success: { bg: '#10b981', color: 'white' }, error: { bg: '#ef4444', color: 'white' }, info: { bg: '#3b82f6', color: 'white' } }; const color = colors[type] || colors.info; notification.style.backgroundColor = color.bg; notification.style.color = color.color; const closeBtn = notification.querySelector('.notification-close'); Object.assign(closeBtn.style, { background: 'none', border: 'none', color: 'inherit', fontSize: '1.5rem', cursor: 'pointer', padding: '0', lineHeight: '1' }); closeBtn.addEventListener('click', () => notification.remove()); document.body.appendChild(notification); setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 5000); } // CSS 애니메이션 정의 const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0.3; transform: translateY(10px); } } .video-card.playing { box-shadow: 0 0 20px rgba(255, 136, 0, 0.3); border: 2px solid rgba(255, 136, 0, 0.5); } .tag:hover { transform: scale(1.1); background: var(--primary-color); color: var(--text-white); cursor: pointer; } `; document.head.appendChild(style);