diff --git a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx index 061a91c..bb0a593 100644 --- a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx +++ b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx @@ -3,7 +3,7 @@ import { useParams, Link, useNavigate } from 'react-router-dom'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useEffect, useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight, ChevronDown, Tv, ExternalLink, Play, MapPin, PartyPopper, ShoppingBag, Ticket, Disc3 } from 'lucide-react'; +import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight, ChevronDown, Tv, ExternalLink, Play, MapPin, PartyPopper, ShoppingBag, Ticket, Disc3, GraduationCap } from 'lucide-react'; import { getSchedule } from '@/api'; import { KakaoMap, MobileLightbox } from '@/components/common'; import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTimeWithTime } from '@/utils'; @@ -567,141 +567,162 @@ function MobileEventSection({ schedule }) { const posters = schedule.posters || []; const postUrls = schedule.postUrls || []; const venue = schedule.venue || null; - const categoryColor = schedule.category?.color || '#facc15'; + const isUniversity = schedule.subtype === 'university'; + const typeLabel = isUniversity ? '대학 축제' : (schedule.category?.name || '행사'); + const ACCENT = '#f59e0b'; const kakaoMapUrl = venue && venue.lat && venue.lng ? `https://map.kakao.com/link/map/${encodeURIComponent(venue.name)},${venue.lat},${venue.lng}` : null; + const [mapOpen, setMapOpen] = useState(false); + const linkLabel = (url) => { + try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; } + }; + const titleShadow = { textShadow: '0 2px 12px rgba(120,53,15,0.45), 0 1px 2px rgba(120,53,15,0.35)' }; + const textShadow = { textShadow: '0 1px 6px rgba(120,53,15,0.4)' }; + + // 포스터 패럴랙스 + 페이드 (스크롤 시 천천히 따라오며 흐려짐) + const posterRef = useRef(null); + useEffect(() => { + if (posters.length === 0) return; + const container = document.querySelector('.mobile-content'); + if (!container) return; + const onScroll = () => { + const el = posterRef.current; + if (!el) return; + const y = container.scrollTop; + const fadeDist = 320; + el.style.transform = `translateY(${(y * 0.4).toFixed(1)}px) scale(${(1 - Math.min(y, fadeDist) / fadeDist * 0.06).toFixed(3)})`; + el.style.opacity = String(Math.max(0, 1 - y / fadeDist)); + }; + onScroll(); + container.addEventListener('scroll', onScroll, { passive: true }); + return () => container.removeEventListener('scroll', onScroll); + }, [posters.length]); return (
- {/* 포스터 */} - {posters.length > 0 ? ( -
-
- {schedule.title} + {/* 포스터 (크게, 분리) — 패럴랙스/페이드 */} + {posters.length > 0 && ( + + {schedule.title} + + )} + + {/* 정보 카드 (그라데이션) */} +
+
+
+
+ + +
+
+ + + {typeLabel} + + {isUniversity && schedule.schoolName && ( + + + {schedule.schoolName} + + )}
- {posters.length > 1 && ( - - ) : ( -
- +
+ + {/* 추가 포스터 */} + {posters.length > 1 && ( +
+ {posters.slice(1).map((p) => ( + + + + ))}
)} - {/* 정보 카드 */} -
-
- - - {schedule.subtype === 'university' ? '대학 축제' : (schedule.category?.name || '행사')} - - - {formatFullDate(schedule.date)} - {schedule.time && ` · ${formatTime(schedule.time)}`} - -
- -

- {decodeHtmlEntities(schedule.title)} -

- - {members.length > 0 && ( -
- {isFullGroup ? ( - - 프로미스나인 - - ) : ( - members.map((member) => ( - - {member.name} - - )) - )} -
- )} - - {venue && ( -
-
- -
-

{venue.name}

- {venue.address && ( -

{venue.address}

- )} + {/* 장소 지도 바텀시트 */} + {createPortal( + + {mapOpen && venue && ( +
+ setMapOpen(false)} /> + +
+
+ +
+

{venue.name}

+ {venue.address &&

{venue.address}

} +
+
+ {kakaoMapUrl && ( - - 카카오맵에서 보기 - + + 카카오맵에서 길찾기 )} -
+
- {venue.lat && venue.lng && ( - - )} -
- )} - - {postUrls.length > 0 && ( -
-

- - 관련 링크 -

-
    - {postUrls.map((url, idx) => ( -
  • - · - - {url} - -
  • - ))} -
-
- )} -
+ )} + , + document.body + )}
); } diff --git a/frontend/src/pages/pc/public/schedule/sections/EventSection.jsx b/frontend/src/pages/pc/public/schedule/sections/EventSection.jsx index 724ee05..1540ff6 100644 --- a/frontend/src/pages/pc/public/schedule/sections/EventSection.jsx +++ b/frontend/src/pages/pc/public/schedule/sections/EventSection.jsx @@ -1,17 +1,20 @@ import { useState } from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Navigation } from 'swiper/modules'; +import { motion, AnimatePresence } from 'framer-motion'; import { Calendar, Clock, MapPin, Link2, PartyPopper, ExternalLink, - ChevronLeft, ChevronRight, + ChevronLeft, ChevronRight, GraduationCap, X, } from 'lucide-react'; import 'swiper/css'; import 'swiper/css/navigation'; import { Lightbox, KakaoMap } from '@/components/common'; import { decodeHtmlEntities, formatFullDate, formatTime } from './utils'; +const ACCENT = '#f59e0b'; + /** - * 행사 일정 섹션 컴포넌트 (학교 행사 등) + * 행사(대학 축제 등) 일정 섹션 — 단일 히어로 통합 디자인 */ function EventSection({ schedule }) { const members = schedule.members || []; @@ -19,186 +22,187 @@ function EventSection({ schedule }) { const posters = schedule.posters || []; const postUrls = schedule.postUrls || []; const venue = schedule.venue || null; - const categoryColor = schedule.category?.color || '#facc15'; + const isUniversity = schedule.subtype === 'university'; + const typeLabel = isUniversity ? '대학 축제' : (schedule.category?.name || '행사'); + const kakaoMapUrl = venue && venue.lat && venue.lng ? `https://map.kakao.com/link/map/${encodeURIComponent(venue.name)},${venue.lat},${venue.lng}` : null; const [lightbox, setLightbox] = useState({ open: false, index: 0 }); + const [mapOpen, setMapOpen] = useState(false); const lightboxImages = posters.map((p) => p.originalUrl || p.mediumUrl); - const openLightbox = (index) => setLightbox({ open: true, index }); + const linkLabel = (url) => { + try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; } + }; + + // 골드 배경 위 텍스트 가독성/입체감용 그림자 + const titleShadow = { textShadow: '0 2px 14px rgba(120,53,15,0.45), 0 1px 2px rgba(120,53,15,0.35)' }; + const textShadow = { textShadow: '0 1px 6px rgba(120,53,15,0.4)' }; + return ( -
- {/* 왼쪽: 포스터 슬라이드 */} -
- {posters.length > 0 ? ( -
- 1 ? { - prevEl: '.event-poster-prev', - nextEl: '.event-poster-next', - } : false} - spaceBetween={0} - slidesPerView={1} - loop={posters.length > 1} - className="w-full" - > - {posters.map((p, idx) => ( - - - - ))} - +
+ {/* ===== 히어로 (모든 정보 통합) ===== */} +
+ {/* 골드 그라데이션 배경 */} +
+
+
+ - {posters.length > 1 && ( - <> - - -
- {posters.length}장 -
- - )} -
- ) : ( -
- -
- )} -
- - {/* 오른쪽: 정보 */} -
- {/* 타입 + 날짜 */} -
- - - {schedule.subtype === 'university' ? '대학 축제' : (schedule.category?.name || '행사')} - - - - {formatFullDate(schedule.date)} - - {schedule.time && ( - - - {formatTime(schedule.time)} - - )} -
- - {/* 제목 */} -

- {decodeHtmlEntities(schedule.title)} -

- - {/* 멤버 */} - {members.length > 0 && ( -
- {isFullGroup ? ( - - 프로미스나인 - - ) : ( - members.map((member) => ( - - {member.name} - - )) - )} -
- )} - - {/* 장소 */} - {venue && ( -
-
- -
-

{venue.name}

- {venue.address && ( -

{venue.address}

- )} - {kakaoMapUrl && ( - - 카카오맵에서 보기 - - + {posters.map((p, idx) => ( + + + + ))} + + {posters.length > 1 && ( + <> + + +
{posters.length}장
+ )}
- {venue.lat && venue.lng && ( - + )} + + {/* 정보 */} +
+
+ + + {typeLabel} + + {isUniversity && schedule.schoolName && ( + + + {schedule.schoolName} + + )} +
+ +

+ {decodeHtmlEntities(schedule.title)} +

+ +
+
+ + {formatFullDate(schedule.date)} + {schedule.time && ( + <> + · + + {formatTime(schedule.time)} + + )} +
+ {venue && ( + + )} +
+ + {members.length > 0 && ( +
+ {isFullGroup ? ( + 프로미스나인 + ) : ( + members.map((m) => ( + {m.name} + )) + )} +
+ )} + + {/* 관련 링크 */} + {postUrls.length > 0 && ( +
+ + + 링크 + + {postUrls.map((url, idx) => ( + + {linkLabel(url)} + + + ))} +
)}
- )} +
+
- {/* URL 목록 */} - {postUrls.length > 0 && ( - +
)} -
+ {/* Lightbox */} {posters.length > 0 && (