diff --git a/frontend/src/pages/mobile/public/Schedule.jsx b/frontend/src/pages/mobile/public/Schedule.jsx index 9be46be..7bcb54c 100644 --- a/frontend/src/pages/mobile/public/Schedule.jsx +++ b/frontend/src/pages/mobile/public/Schedule.jsx @@ -1043,27 +1043,27 @@ function TimelineScheduleCard({ schedule, categoryColor, categories, delay = 0,
- {/* 시간 뱃지 */} - {schedule.time && ( -
-
+ {schedule.time && ( +
{schedule.time.slice(0, 5)}
- - {categoryName} - -
- )} + )} + + {categoryName} + +
{/* 제목 */}

diff --git a/frontend/src/pages/mobile/public/ScheduleDetail.jsx b/frontend/src/pages/mobile/public/ScheduleDetail.jsx index 6fdfbc9..e9465da 100644 --- a/frontend/src/pages/mobile/public/ScheduleDetail.jsx +++ b/frontend/src/pages/mobile/public/ScheduleDetail.jsx @@ -1,8 +1,8 @@ -import { useParams, Link, useNavigate } from 'react-router-dom'; +import { useParams, Link } from 'react-router-dom'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useEffect, useState, useRef } from 'react'; -import { motion } from 'framer-motion'; -import { Calendar, Clock, ChevronLeft, Link2, MapPin, Navigation, ExternalLink } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Calendar, Clock, ChevronLeft, Check, Link2, MapPin, Navigation, ExternalLink } from 'lucide-react'; import { getSchedule, getXProfile } from '../../../api/public/schedules'; import '../../../mobile.css'; @@ -398,11 +398,86 @@ function XSection({ schedule }) { } // 콘서트 섹션 컴포넌트 -function ConcertSection({ schedule, onDateChange }) { - const hasLocation = schedule.location_lat && schedule.location_lng; - const hasPoster = schedule.images?.length > 0; +function ConcertSection({ schedule }) { + // 현재 선택된 회차 ID (내부 state로 관리 - URL 변경 없음) + const [selectedDateId, setSelectedDateId] = useState(schedule.id); + // 다이얼로그 열림 상태 + const [isDialogOpen, setIsDialogOpen] = useState(false); + // 다이얼로그 목록 ref (자동 스크롤용) + const listRef = useRef(null); + const selectedItemRef = useRef(null); + + // 표시할 데이터 state (변경된 부분만 업데이트) + const [displayData, setDisplayData] = useState({ + posterUrl: schedule.images?.[0] || null, + title: schedule.title, + date: schedule.date, + time: schedule.time, + locationName: schedule.location_name, + locationAddress: schedule.location_address, + locationLat: schedule.location_lat, + locationLng: schedule.location_lng, + description: schedule.description, + sourceUrl: schedule.source_url, + }); + + // 선택된 회차 데이터 조회 + const { data: selectedSchedule } = useQuery({ + queryKey: ['schedule', selectedDateId], + queryFn: () => getSchedule(selectedDateId), + placeholderData: keepPreviousData, + enabled: selectedDateId !== schedule.id, + }); + + // 데이터 비교 후 변경된 부분만 업데이트 + useEffect(() => { + const newData = selectedDateId === schedule.id ? schedule : selectedSchedule; + if (!newData) return; + + setDisplayData(prev => { + const updates = {}; + const newPosterUrl = newData.images?.[0] || null; + + if (prev.posterUrl !== newPosterUrl) updates.posterUrl = newPosterUrl; + if (prev.title !== newData.title) updates.title = newData.title; + if (prev.date !== newData.date) updates.date = newData.date; + if (prev.time !== newData.time) updates.time = newData.time; + if (prev.locationName !== newData.location_name) updates.locationName = newData.location_name; + if (prev.locationAddress !== newData.location_address) updates.locationAddress = newData.location_address; + if (prev.locationLat !== newData.location_lat) updates.locationLat = newData.location_lat; + if (prev.locationLng !== newData.location_lng) updates.locationLng = newData.location_lng; + if (prev.description !== newData.description) updates.description = newData.description; + if (prev.sourceUrl !== newData.source_url) updates.sourceUrl = newData.source_url; + + // 변경된 것이 있을 때만 업데이트 + if (Object.keys(updates).length > 0) { + return { ...prev, ...updates }; + } + return prev; + }); + }, [selectedDateId, schedule, selectedSchedule]); + + // 다이얼로그 열릴 때 선택된 항목으로 스크롤 + useEffect(() => { + if (isDialogOpen && selectedItemRef.current) { + setTimeout(() => { + selectedItemRef.current?.scrollIntoView({ block: 'center', behavior: 'instant' }); + }, 50); + } + }, [isDialogOpen]); + const relatedDates = schedule.related_dates || []; const hasMultipleDates = relatedDates.length > 1; + const hasLocation = displayData.locationLat && displayData.locationLng; + + // 현재 선택된 회차 인덱스 + const selectedIndex = relatedDates.findIndex(d => d.id === selectedDateId); + + // 회차 선택 핸들러 + const handleSelectDate = (id) => { + setSelectedDateId(id); + setIsDialogOpen(false); + }; // 개별 날짜 포맷팅 const formatSingleDate = (dateStr, timeStr) => { @@ -420,133 +495,233 @@ function ConcertSection({ schedule, onDateChange }) { }; return ( - - {/* 헤더: 포스터 썸네일 + 제목 */} -
- {hasPoster && ( - {schedule.title} - )} -
-

- {decodeHtmlEntities(schedule.title)} -

-
-
- - {/* 정보 목록 */} -
- {/* 공연 일정 */} -
-
- - 공연 일정 -
- {hasMultipleDates ? ( -
- {relatedDates.map((item, index) => { - const isCurrentDate = item.id === schedule.id; - return ( - - ); - })} + <> +
+ {/* 히어로 헤더 */} +
+ {/* 배경 블러 이미지 */} + {displayData.posterUrl ? ( +
+
) : ( -

- {formatSingleDate(schedule.date, schedule.time)} -

+
)} + {/* 오버레이 그라디언트 */} +
+ + {/* 콘텐츠 */} +
+
+ {/* 포스터 */} + {displayData.posterUrl && ( +
+ {displayData.title} +
+ )} + {/* 제목 */} +

+ {decodeHtmlEntities(displayData.title)} +

+
+
- {/* 장소 */} - {schedule.location_name && ( -
+ {/* 카드 섹션 */} +
+ {/* 공연 일정 카드 */} + +
+ + 공연 일정 +
+ {/* 현재 회차 표시 */} +
+

+ {hasMultipleDates && {selectedIndex + 1}회차 ·} + {formatSingleDate(displayData.date, displayData.time)} +

+
+ {/* 다른 회차 선택 버튼 */} + {hasMultipleDates && ( + + )} +
+ + {/* 장소 카드 */} + {displayData.locationName && ( +
- 장소 + 장소
-

{schedule.location_name}

- {schedule.location_address && ( -

{schedule.location_address}

+

{displayData.locationName}

+ {displayData.locationAddress && ( +

{displayData.locationAddress}

)} -
- )} - {/* 지도 */} - {hasLocation && ( -
-
- - 위치 -
- -
+ {/* 지도 - 좌표가 있으면 카카오맵, 없으면 구글맵 */} + {hasLocation ? ( +
+ +
+ ) : ( +
+