// ========================================
// Portfolio 페이지 전용 JavaScript
// ========================================
document.addEventListener('DOMContentLoaded', function() {
initVideoLazyLoading();
initPortfolioAnimations();
initVideoInteractions();
initYouTubeAPI();
});
// 비디오 레이지 로딩 초기화
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);