2026-01-15 14:42:34 +09:00
|
|
|
import { useParams, Link } from 'react-router-dom';
|
2026-01-15 17:05:33 +09:00
|
|
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
2026-01-16 10:55:40 +09:00
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
import { Calendar, ChevronRight } from 'lucide-react';
|
|
|
|
|
import { getSchedule } from '../../../api/public/schedules';
|
|
|
|
|
|
|
|
|
|
// 분리된 카테고리별 섹션 컴포넌트들
|
|
|
|
|
import {
|
|
|
|
|
YoutubeSection,
|
|
|
|
|
XSection,
|
|
|
|
|
ConcertSection,
|
|
|
|
|
DefaultSection,
|
|
|
|
|
CATEGORY_ID,
|
|
|
|
|
decodeHtmlEntities,
|
|
|
|
|
} from './schedule-sections';
|
2026-01-15 14:42:34 +09:00
|
|
|
|
|
|
|
|
function ScheduleDetail() {
|
|
|
|
|
const { id } = useParams();
|
|
|
|
|
|
2026-01-16 10:55:40 +09:00
|
|
|
const { data: schedule, isLoading, error } = useQuery({
|
2026-01-15 14:42:34 +09:00
|
|
|
queryKey: ['schedule', id],
|
|
|
|
|
queryFn: () => getSchedule(id),
|
2026-01-15 17:05:33 +09:00
|
|
|
placeholderData: keepPreviousData,
|
2026-01-15 20:09:26 +09:00
|
|
|
retry: false, // 404 등 에러 시 재시도하지 않음
|
2026-01-15 14:42:34 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-[calc(100vh-64px)] flex items-center justify-center bg-gray-50">
|
|
|
|
|
<div className="w-10 h-10 border-3 border-primary border-t-transparent rounded-full animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error || !schedule) {
|
|
|
|
|
return (
|
2026-01-15 20:09:26 +09:00
|
|
|
<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"
|
2026-01-15 14:42:34 +09:00
|
|
|
>
|
2026-01-15 20:09:26 +09:00
|
|
|
<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>
|
2026-01-15 14:42:34 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 카테고리별 섹션 렌더링
|
|
|
|
|
const renderCategorySection = () => {
|
|
|
|
|
switch (schedule.category_id) {
|
|
|
|
|
case CATEGORY_ID.YOUTUBE:
|
|
|
|
|
return <YoutubeSection schedule={schedule} />;
|
2026-01-15 14:51:59 +09:00
|
|
|
case CATEGORY_ID.X:
|
|
|
|
|
return <XSection schedule={schedule} />;
|
2026-01-15 15:23:40 +09:00
|
|
|
case CATEGORY_ID.CONCERT:
|
|
|
|
|
return <ConcertSection schedule={schedule} />;
|
2026-01-15 14:42:34 +09:00
|
|
|
default:
|
|
|
|
|
return <DefaultSection schedule={schedule} />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isYoutube = schedule.category_id === CATEGORY_ID.YOUTUBE;
|
2026-01-15 14:51:59 +09:00
|
|
|
const isX = schedule.category_id === CATEGORY_ID.X;
|
2026-01-15 15:23:40 +09:00
|
|
|
const isConcert = schedule.category_id === CATEGORY_ID.CONCERT;
|
|
|
|
|
const hasCustomLayout = isYoutube || isX || isConcert;
|
2026-01-15 14:42:34 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-[calc(100vh-64px)] bg-gray-50">
|
2026-01-16 02:15:32 +09:00
|
|
|
<div className={`${isYoutube ? 'max-w-5xl' : isConcert ? 'max-w-4xl' : 'max-w-3xl'} mx-auto px-6 py-8`}>
|
2026-01-15 14:42:34 +09:00
|
|
|
{/* 브레드크럼 네비게이션 */}
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
2026-01-16 11:02:23 +09:00
|
|
|
className={`flex items-center gap-2 text-sm text-gray-500 mb-8 ${isX ? 'max-w-2xl mx-auto' : ''}`}
|
2026-01-15 14:42:34 +09:00
|
|
|
>
|
|
|
|
|
<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: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: 0.05 }}
|
2026-01-15 14:51:59 +09:00
|
|
|
className={`${hasCustomLayout ? '' : 'bg-white rounded-3xl shadow-sm p-8'}`}
|
2026-01-15 14:42:34 +09:00
|
|
|
>
|
|
|
|
|
{renderCategorySection()}
|
|
|
|
|
</motion.div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ScheduleDetail;
|