From 7d96407bfebd58eafc6f648c6e34ff183d9c792b Mon Sep 17 00:00:00 2001 From: caadiq Date: Thu, 15 Jan 2026 21:20:27 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EC=BD=98?= =?UTF-8?q?=EC=84=9C=ED=8A=B8=20=EC=84=B9=EC=85=98=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8B=AC=EB=A0=A5=20=EC=9D=BC=EC=A0=95=20=EC=A0=90?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 콘서트 UI 최적화: 포스터 썸네일화, 공연 일정 세로 배치 - 카카오맵 SDK 연동으로 실제 지도 표시 - 회차 변경 시 keepPreviousData로 부드러운 전환 - 달력 다른 달 이동 시 일정 점 표시 (비동기 조회) - PC 콘서트 섹션 그림자 통일 (shadow-lg shadow-black/5) Co-Authored-By: Claude Opus 4.5 --- frontend/src/pages/mobile/public/Schedule.jsx | 17 +- .../pages/mobile/public/ScheduleDetail.jsx | 255 +++++++++++++++++- .../src/pages/pc/public/ScheduleDetail.jsx | 8 +- 3 files changed, 269 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/mobile/public/Schedule.jsx b/frontend/src/pages/mobile/public/Schedule.jsx index 9096c9f..9be46be 100644 --- a/frontend/src/pages/mobile/public/Schedule.jsx +++ b/frontend/src/pages/mobile/public/Schedule.jsx @@ -277,6 +277,17 @@ function MobileSchedule() { queryFn: () => getSchedules(viewYear, viewMonth), }); + // 달력 표시용 일정 데이터 (calendarViewDate 기준) + const calendarYear = calendarViewDate.getFullYear(); + const calendarMonth = calendarViewDate.getMonth() + 1; + const isSameMonth = viewYear === calendarYear && viewMonth === calendarMonth; + + const { data: calendarSchedules = [] } = useQuery({ + queryKey: ['schedules', calendarYear, calendarMonth], + queryFn: () => getSchedules(calendarYear, calendarMonth), + enabled: !isSameMonth, // 같은 월이면 중복 조회 안함 + }); + // 생일 폭죽 효과 (하루에 한 번만) useEffect(() => { if (loading || schedules.length === 0) return; @@ -725,9 +736,9 @@ function MobileSchedule() { className="fixed left-0 right-0 bg-white shadow-lg z-50 border-b overflow-hidden" style={{ top: '56px' }} > - { + if (!KAKAO_MAP_KEY) { + setMapError(true); + return; + } + + if (!window.kakao?.maps) { + const script = document.createElement('script'); + script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${KAKAO_MAP_KEY}&autoload=false`; + script.onload = () => { + window.kakao.maps.load(() => setMapLoaded(true)); + }; + script.onerror = () => setMapError(true); + document.head.appendChild(script); + } else { + setMapLoaded(true); + } + }, []); + + useEffect(() => { + if (!mapLoaded || !mapRef.current || mapError) return; + + try { + const position = new window.kakao.maps.LatLng(lat, lng); + const map = new window.kakao.maps.Map(mapRef.current, { + center: position, + level: 3, + }); + + const marker = new window.kakao.maps.Marker({ + position, + map, + }); + + if (name) { + const infowindow = new window.kakao.maps.InfoWindow({ + content: `
${name}
`, + }); + infowindow.open(map, marker); + } + } catch (e) { + setMapError(true); + } + }, [mapLoaded, lat, lng, name, mapError]); + + if (mapError) { + return ( + +
+ +

지도에서 보기

+
+
+ ); + } + + return ( +
+ ); +} + // 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만) function useFullscreenOrientation(isShorts) { useEffect(() => { @@ -319,6 +397,166 @@ function XSection({ schedule }) { ); } +// 콘서트 섹션 컴포넌트 +function ConcertSection({ schedule, onDateChange }) { + const hasLocation = schedule.location_lat && schedule.location_lng; + const hasPoster = schedule.images?.length > 0; + const relatedDates = schedule.related_dates || []; + const hasMultipleDates = relatedDates.length > 1; + + // 개별 날짜 포맷팅 + const formatSingleDate = (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})`; + if (timeStr) { + result += ` ${timeStr.slice(0, 5)}`; + } + return result; + }; + + return ( + + {/* 헤더: 포스터 썸네일 + 제목 */} +
+
+ {hasPoster && ( + {schedule.title} + )} +
+

+ {decodeHtmlEntities(schedule.title)} +

+ {schedule.location_name && ( +

+ {schedule.location_name} +

+ )} +
+
+
+ + {/* 정보 목록 */} +
+ {/* 공연 일정 */} +
+
+ + 공연 일정 +
+ {hasMultipleDates ? ( +
+ {relatedDates.map((item, index) => { + const isCurrentDate = item.id === schedule.id; + return ( + + ); + })} +
+ ) : ( +

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

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

{schedule.location_name}

+ {schedule.location_address && ( +

{schedule.location_address}

+ )} +
+ )} + + {/* 지도 */} + {hasLocation && ( +
+
+ + 위치 +
+ +
+ )} + + {/* 설명 */} + {schedule.description && ( +
+

+ {decodeHtmlEntities(schedule.description)} +

+
+ )} +
+ + {/* 버튼 영역 */} +
+ {/* 길찾기 버튼 */} + {hasLocation && ( + + + 길찾기 + + )} + + {/* 상세 정보 버튼 */} + {schedule.source_url && ( + + + 상세 정보 보기 + + )} +
+
+ ); +} + // 기본 섹션 컴포넌트 (다른 카테고리용 - 임시) function DefaultSection({ schedule }) { return ( @@ -349,6 +587,12 @@ function DefaultSection({ schedule }) { function MobileScheduleDetail() { const { id } = useParams(); + const navigate = useNavigate(); + + // 회차 변경 핸들러 + const handleDateChange = (newId) => { + navigate(`/schedule/${newId}`, { replace: true }); + }; // 모바일 레이아웃 활성화 useEffect(() => { @@ -361,6 +605,7 @@ function MobileScheduleDetail() { const { data: schedule, isLoading, error } = useQuery({ queryKey: ['schedule', id], queryFn: () => getSchedule(id), + placeholderData: keepPreviousData, retry: false, }); @@ -468,6 +713,8 @@ function MobileScheduleDetail() { return ; case CATEGORY_ID.X: return ; + case CATEGORY_ID.CONCERT: + return ; default: return ; } diff --git a/frontend/src/pages/pc/public/ScheduleDetail.jsx b/frontend/src/pages/pc/public/ScheduleDetail.jsx index b56c1b7..45944f0 100644 --- a/frontend/src/pages/pc/public/ScheduleDetail.jsx +++ b/frontend/src/pages/pc/public/ScheduleDetail.jsx @@ -455,7 +455,7 @@ function ConcertSection({ schedule }) { {schedule.title}
@@ -469,7 +469,7 @@ function ConcertSection({ schedule }) { transition={{ delay: 0.2 }} className={`flex-1 ${hasPoster ? '' : 'max-w-xl'}`} > -
+
{/* 헤더 */}

@@ -566,7 +566,7 @@ function ConcertSection({ schedule }) { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }} - className="bg-white rounded-3xl shadow-xl shadow-black/5 overflow-hidden" + className="bg-white rounded-3xl shadow-lg shadow-black/5 overflow-hidden" >
@@ -606,7 +606,7 @@ function ConcertSection({ schedule }) { transition={{ delay: 0.3 }} className="bg-gradient-to-br from-gray-50 to-gray-100 rounded-3xl p-8 text-center" > -
+

{schedule.location_name}