525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
// ========================================
|
|
// 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 = '비디오를 로드할 수 없습니다<br><small>네트워크 연결을 확인해주세요</small>';
|
|
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 = `
|
|
<span class="notification-message">${message}</span>
|
|
<button class="notification-close">×</button>
|
|
`;
|
|
|
|
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); |