diff --git a/frontend/src/pages/pc/public/ScheduleDetail.jsx b/frontend/src/pages/pc/public/ScheduleDetail.jsx index 3c722c9..298085d 100644 --- a/frontend/src/pages/pc/public/ScheduleDetail.jsx +++ b/frontend/src/pages/pc/public/ScheduleDetail.jsx @@ -1,8 +1,8 @@ import { useParams, Link } from 'react-router-dom'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; -import { motion } from 'framer-motion'; -import { Clock, Calendar, ExternalLink, ChevronRight, Link2, MapPin, Navigation } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Clock, Calendar, ExternalLink, ChevronRight, ChevronDown, ChevronLeft, Check, Link2, MapPin, Navigation } from 'lucide-react'; import { getSchedule, getXProfile } from '../../../api/public/schedules'; // 카카오맵 SDK 키 @@ -398,12 +398,12 @@ function KakaoMap({ lat, lng, name }) { href={`https://map.kakao.com/link/to/${encodeURIComponent(name)},${lat},${lng}`} target="_blank" rel="noopener noreferrer" - className="block w-full h-80 rounded-2xl bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center hover:from-gray-200 hover:to-gray-300 transition-all group" + className="group flex h-full w-full items-center justify-center bg-white/80 text-center backdrop-blur transition-all hover:bg-white" > -
- -

{name}

-

클릭하여 길찾기

+
+ +

{name}

+

길찾기 열기

); @@ -412,27 +412,117 @@ function KakaoMap({ lat, lng, name }) { return (
); } // 콘서트 섹션 컴포넌트 function ConcertSection({ schedule }) { - const hasLocation = schedule.location_lat && schedule.location_lng; - const hasPoster = schedule.images?.length > 0; + // 현재 선택된 회차 ID (내부 state로 관리 - URL 변경 없음) + const [selectedDateId, setSelectedDateId] = useState(schedule.id); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + const dropdownListRef = 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(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsDropdownOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // 드롭다운 열릴 때 선택된 항목으로 자동 스크롤 + useEffect(() => { + if (isDropdownOpen && dropdownListRef.current) { + const selectedElement = dropdownListRef.current.querySelector('[data-selected="true"]'); + if (selectedElement) { + // 약간의 지연 후 스크롤 (애니메이션 후) + setTimeout(() => { + selectedElement.scrollIntoView({ block: 'center', behavior: 'smooth' }); + }, 50); + } + } + }, [isDropdownOpen]); + const relatedDates = schedule.related_dates || []; const hasMultipleDates = relatedDates.length > 1; + const hasLocation = displayData.locationLat && displayData.locationLng; + const hasPoster = !!displayData.posterUrl; + const hasDescription = !!displayData.description; - // 개별 날짜 포맷팅 - const formatSingleDate = (dateStr, timeStr) => { + // 현재 선택된 회차 인덱스 + const selectedIndex = relatedDates.findIndex(d => d.id === selectedDateId); + const selectedDisplayIndex = selectedIndex >= 0 ? selectedIndex + 1 : 1; + + // 회차 선택 핸들러 + const handleSelectDate = (id) => { + setSelectedDateId(id); + setIsDropdownOpen(false); + }; + + // 짧은 날짜 포맷팅 (회차 목록용) + const formatShortDate = (dateStr, timeStr) => { const date = new Date(dateStr); const dayNames = ['일', '월', '화', '수', '목', '금', '토']; const month = date.getMonth() + 1; const day = date.getDate(); const weekday = dayNames[date.getDay()]; - let result = `${month}월 ${day}일 (${weekday})`; + let result = `${month}/${day} (${weekday})`; if (timeStr) { result += ` ${timeStr.slice(0, 5)}`; } @@ -440,182 +530,267 @@ function ConcertSection({ schedule }) { }; return ( -
- {/* 상단: 포스터 + 정보 카드 (가로 배치) */} -
- {/* 포스터 이미지 */} - {hasPoster && ( - -
+
+ {/* ========== 히어로 섹션 ========== */} + + {/* 배경 레이어 - 포스터 확대 + 블러 */} +
+ {hasPoster ? ( + <> {schedule.title} -
-
- - )} +
+ + ) : ( +
+ )} +
- {/* 정보 카드 */} - +
+ {/* 포스터 */} + {hasPoster && ( + + {displayData.title} + + )} + + {/* 제목 및 회차 선택 */} +
+ {/* 제목 */} + + {decodeHtmlEntities(displayData.title)} + + + {/* 회차 선택 드롭다운 */} + {hasMultipleDates && ( + + + + + {isDropdownOpen && ( + + {/* 드롭다운 헤더 */} +
+ 공연 일정 선택 +
+ +
+ {relatedDates.map((item, index) => { + const isSelected = item.id === selectedDateId; + return ( + + ); + })} +
+
+ )} +
+
+ )} +
+
+
+ + + {/* ========== 장소 정보 카드 ========== */} + {displayData.locationName && ( + -
- {/* 헤더 */} -
-

- {decodeHtmlEntities(schedule.title)} -

-
- - {/* 정보 목록 */} -
- {/* 공연 일정 목록 */} -
-
- + {/* 헤더 */} +
+
+
+ {/* 장소 아이콘 */} +
+
-
- {hasMultipleDates ? ( -
- {relatedDates.map((item, index) => { - const isCurrentDate = item.id === schedule.id; - return ( - - - {index + 1}회차 - - {formatSingleDate(item.date, item.time)} - - ); - })} -
- ) : ( -
-

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

-
+ {/* 텍스트 */} +
+

{displayData.locationName}

+ {displayData.locationAddress && ( +

{displayData.locationAddress}

)}
- - {/* 장소 */} - {schedule.location_name && ( -
-
- -
-
-

{schedule.location_name}

- {schedule.location_address && ( -

{schedule.location_address}

- )} -
-
- )} - - {/* 설명 */} - {schedule.description && ( -
-

- {decodeHtmlEntities(schedule.description)} -

-
- )} + + {/* 길찾기 버튼 - 카카오맵(국내) / 구글맵(해외) */} + + + 길찾기 +
+
- {/* 버튼 영역 */} - {schedule.source_url && ( - + {/* 지도 - 높이 2배 */} +
+ {hasLocation ? ( + + ) : ( +