import { useParams, Link } from 'react-router-dom'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useEffect, useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react'; import { getSchedule } from '@/api'; import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTimeWithTime } from '@/utils'; import Birthday from './Birthday'; /** * URL을 링크로 변환하는 함수 */ function linkifyText(text) { if (!text) return null; // URL 패턴: http(s)://로 시작하거나 일반적인 단축 URL 도메인 const urlPattern = /(https?:\/\/[^\s]+|(?:bit\.ly|youtu\.be|t\.co|goo\.gl|tinyurl\.com)\/[^\s]+)/gi; const parts = []; let lastIndex = 0; let match; while ((match = urlPattern.exec(text)) !== null) { if (match.index > lastIndex) { parts.push(text.slice(lastIndex, match.index)); } let url = match[0]; const href = url.startsWith('http') ? url : `https://${url}`; parts.push( {url} ); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { parts.push(text.slice(lastIndex)); } return parts.length > 0 ? parts : text; } /** * 특수 일정 ID 파싱 * @param {string} id - 일정 ID * @returns {object|null} { type, year, nameEn } 또는 null */ function parseSpecialId(id) { // birthday-{year}-{nameEn} 형식 const birthdayMatch = id.match(/^birthday-(\d{4})-(.+)$/); if (birthdayMatch) { return { type: 'birthday', year: birthdayMatch[1], nameEn: birthdayMatch[2] }; } // debut-{year} 형식 const debutMatch = id.match(/^debut-(\d{4})$/); if (debutMatch) { return { type: 'debut', year: debutMatch[1] }; } // anniversary-{year} 형식 const anniversaryMatch = id.match(/^anniversary-(\d{4})$/); if (anniversaryMatch) { return { type: 'anniversary', year: anniversaryMatch[1] }; } return null; } /** * 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만) */ function useFullscreenOrientation(isShorts) { useEffect(() => { if (isShorts) return; const handleFullscreenChange = async () => { const isFullscreen = !!document.fullscreenElement; if (isFullscreen) { try { if (screen.orientation && screen.orientation.lock) { await screen.orientation.lock('landscape'); } } catch (e) { // 지원하지 않는 브라우저 } } else { try { if (screen.orientation && screen.orientation.unlock) { screen.orientation.unlock(); } } catch (e) { // 무시 } } }; document.addEventListener('fullscreenchange', handleFullscreenChange); document.addEventListener('webkitfullscreenchange', handleFullscreenChange); return () => { document.removeEventListener('fullscreenchange', handleFullscreenChange); document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); }; }, [isShorts]); } /** * Mobile 예정 일정 Placeholder 컴포넌트 */ function MobileScheduledPlaceholder({ bannerUrl }) { return (
{/* 배경: 배너 이미지 또는 패턴 */} {bannerUrl ? (
) : (
)} {/* 하단 텍스트 */}
업로드 예정
); } /** * Mobile 유튜브 섹션 */ function MobileYoutubeSection({ schedule }) { const videoId = schedule.videoId; const isShorts = schedule.videoType === 'shorts'; const isScheduled = !videoId; // videoId가 없으면 예정 일정 // 숏츠가 아닐 때만 가로 회전 (숏츠는 전체화면에서 세로 유지) useFullscreenOrientation(isShorts); const members = schedule.members || []; const isFullGroup = members.length === 5; return (
{/* 영상 임베드 또는 예정 Placeholder */} {isScheduled ? ( ) : (