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, Tv, ExternalLink, Play } 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 매칭
const pattern = /(#[^\s#]+)|(https?:\/\/[^\s]+|(?:bit\.ly|youtu\.be|t\.co|goo\.gl|tinyurl\.com)\/[^\s]+)/gi;
const parts = [];
let lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
const matched = match[0];
if (matched.startsWith('#')) {
// 해시태그
const tag = matched.slice(1);
parts.push(
{matched}
);
} else {
// URL
const href = matched.startsWith('http') ? matched : `https://${matched}`;
parts.push(
{matched}
);
}
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 ? (
) : (
)}
{/* 영상 정보 */}
{decodeHtmlEntities(schedule.title)}
{isScheduled && (
예정
)}
{/* 메타 정보 */}
0 || !isScheduled ? 'mb-3' : ''}`}>
{formatXDateTimeWithTime(schedule.date, schedule.time)}
{schedule.channelName && (
{schedule.channelName}
)}
{/* 멤버 목록 */}
{members.length > 0 && (
{isFullGroup ? (
프로미스나인
) : (
members.map((member) => (
{member.name}
))
)}
)}
{/* 유튜브에서 보기 버튼 (예정 일정이 아닐 때만) */}
{!isScheduled && (
)}
);
}
/**
* Mobile X(트위터) 섹션
*/
function MobileXSection({ schedule }) {
const profile = schedule.profile;
const username = profile?.username || 'realfromis_9';
const displayName = profile?.displayName || username;
const avatarUrl = profile?.avatarUrl;
// 라이트박스 상태
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const historyPushedRef = useRef(false);
const openLightbox = (index) => {
setLightboxIndex(index);
setLightboxOpen(true);
window.history.pushState({ lightbox: true }, '');
historyPushedRef.current = true;
};
const closeLightbox = () => {
setLightboxOpen(false);
if (historyPushedRef.current) {
historyPushedRef.current = false;
window.history.back();
}
};
const goToPrev = () => {
if (schedule.imageUrls?.length > 1) {
setLightboxIndex((lightboxIndex - 1 + schedule.imageUrls.length) % schedule.imageUrls.length);
}
};
const goToNext = () => {
if (schedule.imageUrls?.length > 1) {
setLightboxIndex((lightboxIndex + 1) % schedule.imageUrls.length);
}
};
// 라이트박스 열릴 때 body 스크롤 방지
useEffect(() => {
if (lightboxOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [lightboxOpen]);
// 뒤로가기 처리 (하드웨어 백버튼)
useEffect(() => {
const handlePopState = () => {
if (lightboxOpen) {
historyPushedRef.current = false;
setLightboxOpen(false);
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [lightboxOpen]);
return (
<>
{/* 헤더 */}
{avatarUrl ? (

) : (
{displayName.charAt(0).toUpperCase()}
)}
{/* 본문 */}
{linkifyText(decodeHtmlEntities(schedule.content || schedule.title))}
{/* 이미지 */}
{schedule.imageUrls?.length > 0 && (
{schedule.imageUrls.length === 1 ? (

openLightbox(0)}
/>
) : (
{schedule.imageUrls.slice(0, 4).map((url, i) => (

openLightbox(i)}
/>
))}
)}
)}
{/* 날짜/시간 */}
{formatXDateTimeWithTime(schedule.date, schedule.time)}
{/* X에서 보기 버튼 */}
{/* 모바일 라이트박스 */}
{lightboxOpen && schedule.imageUrls?.length > 0 && (
e.stopPropagation()}
/>
{schedule.imageUrls.length > 1 && (
<>
>
)}
{schedule.imageUrls.length > 1 && (
{schedule.imageUrls.map((_, i) => (
)}
)}
>
);
}
/**
* Mobile 예능 섹션
*/
function MobileVarietySection({ schedule }) {
const members = schedule.members || [];
const isFullGroup = members.length === 5;
const hasThumbnail = !!schedule.thumbnailUrl;
const hasReplayUrl = !!schedule.replayUrl;
const isYoutubeReplay = hasReplayUrl && /youtu\.?be/i.test(schedule.replayUrl);
return (
{/* 썸네일 */}
{hasThumbnail ? (

) : (
)}
{/* 콘텐츠 */}
{/* 상단 */}
{/* 방송사 + 날짜 */}
{schedule.broadcaster && (
{schedule.broadcaster}
)}
{formatFullDate(schedule.date)}
{/* 제목 */}
{decodeHtmlEntities(schedule.title)}
{/* 멤버 */}
{members.length > 0 && (
{isFullGroup ? (
프로미스나인
) : (
members.map((member) => (
{member.name}
))
)}
)}
{/* 다시보기 */}
{hasReplayUrl && (
)}
);
}
/**
* Mobile 기본 섹션
*/
function MobileDefaultSection({ schedule }) {
return (
{decodeHtmlEntities(schedule.title)}
{formatFullDate(schedule.date)}
{schedule.time && (
{formatTime(schedule.time)}
)}
{schedule.description && (
{decodeHtmlEntities(schedule.description)}
)}
);
}
/**
* Mobile 일정 상세 페이지
*/
function MobileScheduleDetail() {
const { id } = useParams();
// 특수 일정 ID 체크
const specialId = parseSpecialId(id);
if (specialId?.type === 'birthday') {
return ;
}
// 모바일 레이아웃 활성화
useEffect(() => {
document.documentElement.classList.add('mobile-layout');
return () => {
document.documentElement.classList.remove('mobile-layout');
};
}, []);
const {
data: schedule,
isLoading,
error,
} = useQuery({
queryKey: ['schedule', id],
queryFn: () => getSchedule(id),
placeholderData: keepPreviousData,
retry: false,
});
if (isLoading && !schedule) {
return (
);
}
if (error || !schedule) {
return (
일정을 찾을 수 없습니다
요청하신 일정이 존재하지 않거나
삭제되었을 수 있습니다.
{[...Array(5)].map((_, i) => (
))}
일정 목록
);
}
// 카테고리별 섹션 렌더링
const categoryName = schedule.category?.name;
const renderCategorySection = () => {
switch (categoryName) {
case '유튜브':
return ;
case 'X':
return ;
case '예능':
return ;
default:
return ;
}
};
return (
{/* 헤더 */}
{schedule.category?.name}
{/* 메인 컨텐츠 */}
{renderCategorySection()}
);
}
export default MobileScheduleDetail;