- VarietySection: 썸네일, 방송사 뱃지, 제목, 날짜/시간, 멤버, 다시보기 버튼 - ScheduleDetail에서 '예능' 카테고리 분기 연결 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
211 lines
6.9 KiB
JavaScript
211 lines
6.9 KiB
JavaScript
import { useParams, Link } from 'react-router-dom';
|
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
|
import { motion } from 'framer-motion';
|
|
import { Calendar, ChevronRight } from 'lucide-react';
|
|
import { getSchedule } from '@/api';
|
|
|
|
// 섹션 컴포넌트들
|
|
import { YoutubeSection, XSection, VarietySection, DefaultSection, decodeHtmlEntities } from './sections';
|
|
import Birthday from './Birthday';
|
|
|
|
/**
|
|
* 특수 일정 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;
|
|
}
|
|
|
|
/**
|
|
* PC 일정 상세 페이지
|
|
*/
|
|
function PCScheduleDetail() {
|
|
const { id } = useParams();
|
|
|
|
// 특수 일정 ID 체크
|
|
const specialId = parseSpecialId(id);
|
|
if (specialId?.type === 'birthday') {
|
|
return <Birthday year={specialId.year} nameEn={specialId.nameEn} />;
|
|
}
|
|
|
|
const {
|
|
data: schedule,
|
|
isLoading,
|
|
error,
|
|
} = useQuery({
|
|
queryKey: ['schedule', id],
|
|
queryFn: () => getSchedule(id),
|
|
placeholderData: keepPreviousData,
|
|
retry: false,
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <div className="min-h-[calc(100vh-64px)] bg-gray-50" />;
|
|
}
|
|
|
|
if (error || !schedule) {
|
|
return (
|
|
<div className="min-h-[calc(100vh-64px)] flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
|
|
<div className="text-center px-6">
|
|
{/* 아이콘 */}
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.5 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.5, type: 'spring', stiffness: 100 }}
|
|
className="mb-8"
|
|
>
|
|
<div className="w-32 h-32 mx-auto bg-gradient-to-br from-primary/10 to-primary/5 rounded-3xl flex items-center justify-center">
|
|
<Calendar size={64} className="text-primary/40" />
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* 메시지 */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.2, duration: 0.5 }}
|
|
className="mb-8"
|
|
>
|
|
<h2 className="text-2xl font-bold text-gray-800 mb-3">일정을 찾을 수 없습니다</h2>
|
|
<p className="text-gray-500 leading-relaxed">
|
|
요청하신 일정이 존재하지 않거나 삭제되었을 수 있습니다.
|
|
<br />
|
|
다른 일정을 확인해 주세요.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* 장식 요소 */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.4, duration: 0.5 }}
|
|
className="flex justify-center gap-2 mb-10"
|
|
>
|
|
{[...Array(5)].map((_, i) => (
|
|
<motion.div
|
|
key={i}
|
|
className="w-2 h-2 rounded-full bg-primary"
|
|
animate={{
|
|
y: [0, -8, 0],
|
|
opacity: [0.3, 1, 0.3],
|
|
}}
|
|
transition={{
|
|
duration: 1.2,
|
|
repeat: Infinity,
|
|
delay: i * 0.15,
|
|
ease: 'easeInOut',
|
|
}}
|
|
/>
|
|
))}
|
|
</motion.div>
|
|
|
|
{/* 버튼들 */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.5, duration: 0.5 }}
|
|
className="flex justify-center gap-4"
|
|
>
|
|
<button
|
|
onClick={() => window.history.back()}
|
|
className="flex items-center gap-2 px-6 py-3 border-2 border-primary text-primary rounded-full font-medium hover:bg-primary hover:text-white transition-colors duration-200"
|
|
>
|
|
<ChevronRight size={18} className="rotate-180" />
|
|
이전 페이지
|
|
</button>
|
|
<Link
|
|
to="/schedule"
|
|
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-white rounded-full font-medium hover:shadow-lg hover:shadow-primary/30 transition-all duration-200"
|
|
>
|
|
<Calendar size={18} />
|
|
일정 목록
|
|
</Link>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 카테고리별 섹션 렌더링
|
|
const categoryName = schedule.category?.name;
|
|
const renderCategorySection = () => {
|
|
switch (categoryName) {
|
|
case '유튜브':
|
|
return <YoutubeSection schedule={schedule} />;
|
|
case 'X':
|
|
return <XSection schedule={schedule} />;
|
|
case '예능':
|
|
return <VarietySection schedule={schedule} />;
|
|
default:
|
|
return <DefaultSection schedule={schedule} />;
|
|
}
|
|
};
|
|
|
|
const isYoutube = categoryName === '유튜브';
|
|
const isX = categoryName === 'X';
|
|
const isVariety = categoryName === '예능';
|
|
const hasCustomLayout = isYoutube || isX || isVariety;
|
|
|
|
return (
|
|
<div className="min-h-[calc(100vh-64px)] bg-gray-50">
|
|
<div className={`${isYoutube ? 'max-w-5xl' : 'max-w-3xl'} mx-auto px-6 py-8`}>
|
|
{/* 브레드크럼 네비게이션 */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{
|
|
duration: 0.3,
|
|
ease: [0.25, 0.1, 0.25, 1],
|
|
delay: 0.1,
|
|
}}
|
|
className={`flex items-center gap-2 text-sm text-gray-500 mb-8 ${isX ? 'max-w-2xl mx-auto' : ''}`}
|
|
>
|
|
<Link to="/schedule" className="hover:text-primary transition-colors">
|
|
일정
|
|
</Link>
|
|
<ChevronRight size={14} />
|
|
<span className="hover:text-primary transition-colors" style={{ color: schedule.category?.color }}>
|
|
{schedule.category?.name}
|
|
</span>
|
|
<ChevronRight size={14} />
|
|
<span className="text-gray-700 font-medium truncate max-w-md">{decodeHtmlEntities(schedule.title)}</span>
|
|
</motion.div>
|
|
|
|
{/* 메인 컨텐츠 */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{
|
|
duration: 0.4,
|
|
ease: [0.25, 0.1, 0.25, 1],
|
|
delay: 0.15,
|
|
}}
|
|
className={`${hasCustomLayout ? '' : 'bg-white rounded-3xl shadow-sm p-8'}`}
|
|
>
|
|
{renderCategorySection()}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default PCScheduleDetail;
|