diff --git a/src/assets/fonts/subset-PyeongChangPeace-Bold.woff2 b/src/assets/fonts/subset-PyeongChangPeace-Bold.woff2 new file mode 100644 index 0000000..afe0917 Binary files /dev/null and b/src/assets/fonts/subset-PyeongChangPeace-Bold.woff2 differ diff --git a/src/assets/fonts/subset-PyeongChangPeace-Light.woff2 b/src/assets/fonts/subset-PyeongChangPeace-Light.woff2 new file mode 100644 index 0000000..b606d8d Binary files /dev/null and b/src/assets/fonts/subset-PyeongChangPeace-Light.woff2 differ diff --git a/src/components/ImageModal/index.css.ts b/src/components/ImageModal/index.css.ts index 1d392e5..176023d 100644 --- a/src/components/ImageModal/index.css.ts +++ b/src/components/ImageModal/index.css.ts @@ -1,13 +1,15 @@ import { style } from "@vanilla-extract/css"; export const container = style({ + position: "relative", width: "100%", height: "100%", }); export const zoomInBtn = style({ position: "absolute", + top: "5px", + left: "5px", cursor: "pointer", - margin: "5px", padding: "3px", borderRadius: "5px", backgroundColor: "rgba(255, 255, 255, 0.3)", @@ -17,3 +19,13 @@ export const image = style({ height: "100%", objectFit: "cover", }); + +export const placeBtn = style({ + position: "absolute", + top: "5px", + right: "5px", + cursor: "pointer", + padding: "3px", + borderRadius: "5px", + backgroundColor: "rgba(255, 255, 255, 0.3)", +}); diff --git a/src/components/ImageModal/index.tsx b/src/components/ImageModal/index.tsx index e48cffe..ddb3d53 100644 --- a/src/components/ImageModal/index.tsx +++ b/src/components/ImageModal/index.tsx @@ -1,6 +1,31 @@ import * as styles from "./index.css.ts"; -export default function ImageModal({ src, alt }: { src: string; alt: string }) { +interface ImageModalProps { + src: string; + alt: string; + activePlace?: "60주년기념관" | "함인섭광장" | "대운동장"; +} + +export default function ImageModal({ src, alt, activePlace }: ImageModalProps) { + const placeLinks: { [key: string]: string } = { + "60주년기념관": "https://kko.to/hKoJW3SEaa", + 함인섭광장: "https://kko.to/KvvTprkn8T", + 대운동장: "https://kko.to/MkBZQvfuH0", + }; + + const handlePlaceButtonClick = () => { + if (activePlace) { + const link = placeLinks[activePlace]; + if (link) { + window.location.href = link; + } else { + console.error(`Link for ${activePlace} is not defined.`); + } + } else { + console.error("activePlace is not provided."); + } + }; + return (
zoom_in + {alt === "layout" && ( + + map + + )} {alt}
); diff --git a/src/pages/BoothNFoodDetail/Overview/index.tsx b/src/pages/BoothNFoodDetail/Overview/index.tsx index dd4b188..e6785fa 100644 --- a/src/pages/BoothNFoodDetail/Overview/index.tsx +++ b/src/pages/BoothNFoodDetail/Overview/index.tsx @@ -1,4 +1,5 @@ import ImageModal from "../../../components/ImageModal/index.tsx"; +import { BoothPlaceType } from "../../../shared/types/asset_types.ts"; import * as styles from "./index.css.ts"; type OverviewProps = { @@ -6,7 +7,7 @@ type OverviewProps = { date: number[]; imgURL: string; order: number; - place: string; + place: BoothPlaceType; }; export default function Overview({ title, date, imgURL, order, place }: OverviewProps) { @@ -20,7 +21,13 @@ export default function Overview({ title, date, imgURL, order, place }: Overview
schedule - {title.includes("주점") ? "18:00 ~ 01:00" : "11:00 ~ 17:00"} + + {title === "주점" + ? place === "미래광장" + ? "18:00 ~ 00:00" + : "18:00 ~ 01:00" + : "11:00 ~ 17:00"} +
date_range diff --git a/src/pages/BoothNFoodList/index.tsx b/src/pages/BoothNFoodList/index.tsx index 8d83478..651cb6b 100644 --- a/src/pages/BoothNFoodList/index.tsx +++ b/src/pages/BoothNFoodList/index.tsx @@ -29,15 +29,15 @@ export default function BoothNFoodList() {
{activePlace !== "미래광장" && ( )}
diff --git a/src/pages/Notice/index.tsx b/src/pages/Notice/index.tsx index f888fb7..22f13e0 100644 --- a/src/pages/Notice/index.tsx +++ b/src/pages/Notice/index.tsx @@ -101,19 +101,25 @@ function Notice() { ) : ( currentNotices.map((noticeItem, index) => (
-
+
toggleNotice(noticeItem.id)} className={notice}>

{index + 1 + (currentPage - 1) * itemsPerPage}

{/* 공지사항의 renewal이 true일 경우 'New' 배지를 표시 */} {noticeItem.renewal && 중요}
-
toggleNotice(noticeItem.id)} className={noticeContentWrapper}> +

{noticeItem.title}

{/* downbtn을 클릭하면 토글 */} - -)); - -const ArtistInfo: React.FC<{ - artist: artistInfoListProps[]; -}> = React.memo(({ artist }) => ( -
-

상세정보

- {artist.map((a: artistInfoListProps, index) => ( - - {a.name} -

{a.name}

- - ))} -
-)); - -const TimeTableItem: React.FC<{ - timeTable: timeTableInfoProps; - currentTime: Date; - refCallback: (node: HTMLDivElement | null) => void; -}> = React.memo(({ timeTable, currentTime, refCallback }) => { - const status = getEventStatus(timeTable, currentTime); - - return ( -
-

{timeTable.title}

- {timeTable.descriptionShow && ( -

{timeTable.description}

- )} -

- {timeTable.startTime.getMonth() + 1}/{timeTable.startTime.getDate()} |{" "} - {formatTime(timeTable.startTime)} ~ {formatTime(timeTable.endTime)} -

- {timeTable.artist && } - {status === "current" &&
진행 중
} -
- ); -}); - -// Main component +/** + * 타임테이블 페이지 + */ export default function Timetable() { const [viewTime, setViewTime] = useState(START_DATE); const [currentTime, setCurrentTime] = useState(new Date()); const lastCurrentEventRef = useRef(null); + /** + * 현재 날짜에 맞게 기준시간 설정 + */ useEffect(() => { const today = clearTime(new Date()); const start = clearTime(START_DATE); @@ -120,6 +34,11 @@ export default function Timetable() { return () => clearInterval(timer); }, []); + /** + * 필터링된 타임테이블 정보 + * @see timeTableInfo + * @see useMemo + */ const filteredTimeTableInfo = useMemo( () => timeTableInfo @@ -128,23 +47,43 @@ export default function Timetable() { [viewTime], ); + /** + * 현재 진행중인 이벤트로 스크롤 + */ useEffect(() => { - // 진행 중인 마지막 요소로 스크롤 - if (lastCurrentEventRef.current) { - if ("scrollIntoView" in lastCurrentEventRef.current) { - lastCurrentEventRef.current.scrollIntoView({ behavior: "smooth" }); - return; - } + if ( + lastCurrentEventRef.current && + clearTime(currentTime).getTime() === clearTime(viewTime).getTime() + ) { + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const offset = rootFontSize * 3.75; + + const elementPosition = + lastCurrentEventRef.current!.getBoundingClientRect().top + window.scrollY; + const offsetPosition = elementPosition - offset; + + window.scrollTo({ + top: offsetPosition, + behavior: "smooth", + }); + } else { + window.scrollTo(0, 0); } - window.scrollTo(0, 0); }, [filteredTimeTableInfo]); + /** + * 필터 클릭 이벤트 + */ const handleFilterClick = useCallback((date: Date) => { setViewTime(clearTime(date)); }, []); + /** + * 렌더링 + */ return (
+ {/*상단 필터링 버튼*/}
{TIME_TABLE_FILTER.map((timeTable, index) => ( ))}
+ {/*필터링된 타임테이블 정보*/}
{filteredTimeTableInfo.map((timeTable, index) => ( @@ -169,6 +109,7 @@ export default function Timetable() { ))}
+ {/*하단 필터링 버튼*/}
{TIME_TABLE_FILTER.map((timeTable, index) => ( void; + isActive: boolean; +}> = React.memo(({ timeTable, onClick, isActive }) => ( + +)); + +/** + * 아티스트 정보 + * @param artist 아티스트 정보 + * @see artistInfoListProps + */ +const ArtistInfo: React.FC<{ + artist: artistInfoListProps[]; +}> = React.memo(({ artist }) => ( +
+

상세정보

+ {artist.map((a: artistInfoListProps, index) => ( + + {a.name} +

{a.name}

+ + ))} +
+)); + +/** + * 타임테이블 아이템 + * @param timeTable 타임테이블 정보 + * @param currentTime 현재 시간 + * @param refCallback 현재 진행중인 이벤트를 가리키기 위한 콜백 + */ +export const TimeTableItem: React.FC<{ + timeTable: timeTableInfoProps; + currentTime: Date; + refCallback: (node: HTMLDivElement | null) => void; +}> = React.memo(({ timeTable, currentTime, refCallback }) => { + const status = getEventStatus(timeTable, currentTime); + + return ( +
+

{timeTable.title}

+ {timeTable.descriptionShow && ( +

+ {timeTable.description} + {timeTable.link && ( + + {timeTable.link.text} + + )} +

+ )} +

+ {timeTable.startTime.getMonth() + 1}/{timeTable.startTime.getDate()} |{" "} + {formatTime(timeTable.startTime)} ~ {formatTime(timeTable.endTime)} +

+ {timeTable.artist && } + {status === "current" &&
진행 중
} +
+ ); +}); diff --git a/src/pages/Timetable/timeTableInfo.ts b/src/pages/Timetable/timeTableInfo.ts index d892a7f..aa88aea 100644 --- a/src/pages/Timetable/timeTableInfo.ts +++ b/src/pages/Timetable/timeTableInfo.ts @@ -387,85 +387,133 @@ export const timeTableInfo: timeTableInfoProps[] = [ endTime: new Date("2024-09-24 20:14"), }, { - title: "재학생 우선 입장", - description: "재학생 우선 입장 시간 (자세한 안내는 인스타그램 참조)", + title: "무대 입장(재학생)", + description: "팔찌 착용 필요", descriptionShow: true, date: new Date("2024-09-23"), startTime: new Date("2024-09-23 17:00"), endTime: new Date("2024-09-23 17:50"), + link: { + text: "(자세히보기)", + url: "https://www.instagram.com/p/DAFpPQ_SQyL", + }, }, { - title: "전체 입장", - description: "전체 입장 시간", - descriptionShow: true, + title: "무대 입장(전체)", + description: "무대 입장(전체) 시간", + descriptionShow: false, date: new Date("2024-09-23"), startTime: new Date("2024-09-23 17:50"), endTime: new Date("2024-09-23 24:00"), }, { - title: "재학생 우선 입장", - description: "재학생 우선 입장 시간 (자세한 안내는 인스타그램 참조)", + title: "무대 입장(재학생)", + description: "팔찌 착용 필요", descriptionShow: true, date: new Date("2024-09-24"), startTime: new Date("2024-09-24 17:00"), endTime: new Date("2024-09-24 17:50"), + link: { + text: "(자세히보기)", + url: "https://www.instagram.com/p/DAFpPQ_SQyL", + }, }, { - title: "전체 입장", - description: "전체 입장 시간", - descriptionShow: true, + title: "무대 입장(전체)", + description: "무대 입장(전체) 시간", + descriptionShow: false, date: new Date("2024-09-24"), startTime: new Date("2024-09-24 17:50"), endTime: new Date("2024-09-24 24:00"), }, { - title: "재학생 우선 입장", - description: "재학생 우선 입장 시간 (자세한 안내는 인스타그램 참조)", + title: "무대 입장(재학생)", + description: "팔찌 착용 필요", descriptionShow: true, date: new Date("2024-09-25"), startTime: new Date("2024-09-25 17:00"), endTime: new Date("2024-09-25 17:50"), + link: { + text: "(자세히보기)", + url: "https://www.instagram.com/p/DAFpPQ_SQyL", + }, }, { - title: "전체 입장", - description: "전체 입장 시간", - descriptionShow: true, + title: "무대 입장(전체)", + description: "무대 입장(전체) 시간", + descriptionShow: false, date: new Date("2024-09-25"), startTime: new Date("2024-09-25 17:50"), endTime: new Date("2024-09-25 24:00"), }, { - title: "주점 개시", + title: "미래광장 주점 운영", + description: "", + descriptionShow: false, + artist: [futurePubInfo], + date: new Date("2024-09-23"), + startTime: new Date("2024-09-23 18:00"), + endTime: new Date("2024-09-24 00:00"), + }, + { + title: "60주년기념관 주점 운영", description: "", descriptionShow: false, - artist: [futurePubInfo, sixtyPubInfo], + artist: [sixtyPubInfo], date: new Date("2024-09-23"), startTime: new Date("2024-09-23 18:00"), endTime: new Date("2024-09-24 01:00"), }, { - title: "주점 개시", + title: "미래광장 주점 운영", description: "", descriptionShow: false, - artist: [futurePubInfo, sixtyPubInfo], + artist: [futurePubInfo], + date: new Date("2024-09-24"), + startTime: new Date("2024-09-24 18:00"), + endTime: new Date("2024-09-25 00:00"), + }, + { + title: "60주년기념관 주점 운영", + description: "", + descriptionShow: false, + artist: [sixtyPubInfo], date: new Date("2024-09-24"), startTime: new Date("2024-09-24 18:00"), endTime: new Date("2024-09-25 01:00"), }, { - title: "주점 개시", + title: "미래광장 주점 운영", description: "", descriptionShow: false, - artist: [futurePubInfo, sixtyPubInfo], + artist: [futurePubInfo], date: new Date("2024-09-25"), startTime: new Date("2024-09-25 18:00"), - endTime: new Date("2024-09-25 01:00"), + endTime: new Date("2024-09-26 00:00"), + }, + { + title: "60주년기념관 주점 운영", + description: "", + descriptionShow: false, + artist: [sixtyPubInfo], + date: new Date("2024-09-25"), + startTime: new Date("2024-09-25 18:00"), + endTime: new Date("2024-09-26 01:00"), + }, + { + title: "미래광장 주점 운영", + description: "", + descriptionShow: false, + artist: [futurePubInfo], + date: new Date("2024-09-26"), + startTime: new Date("2024-09-26 18:00"), + endTime: new Date("2024-09-27 00:00"), }, { - title: "주점 개시", + title: "60주년기념관 주점 운영", description: "", descriptionShow: false, - artist: [futurePubInfo, sixtyPubInfo], + artist: [sixtyPubInfo], date: new Date("2024-09-26"), startTime: new Date("2024-09-26 18:00"), endTime: new Date("2024-09-27 01:00"), diff --git a/src/pages/Timetable/utils.tsx b/src/pages/Timetable/utils.tsx new file mode 100644 index 0000000..9b7e298 --- /dev/null +++ b/src/pages/Timetable/utils.tsx @@ -0,0 +1,64 @@ +import { timeTableFilterProps, timeTableInfoProps } from "../../shared/types/timeTable.ts"; + +/** + * 일별 필터 버튼 + * name: 버튼 이름 + * date: 필터링할 날짜 + */ +export const TIME_TABLE_FILTER: timeTableFilterProps[] = [ + { name: "1일차", date: new Date("2024-09-23T00:00:00+09:00") }, + { name: "2일차", date: new Date("2024-09-24T00:00:00+09:00") }, + { name: "3일차", date: new Date("2024-09-25T00:00:00+09:00") }, + { name: "4일차", date: new Date("2024-09-26T00:00:00+09:00") }, +]; + +/** + * 축제 시작일 + */ +export const START_DATE = new Date("2024-09-23T00:00:00+09:00"); + +/** + * 축제 종료일 + */ +export const END_DATE = new Date("2024-09-26T00:00:00+09:00"); + +/** + * 시간을 00:00:00으로 설정 (날짜만 유지, 시간 초기화) + * @param date 날짜 + */ +export const clearTime = (date: Date): Date => { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +}; + +/** + * Date 객체가 들어왔을때 시간을 mm:hh 형식으로 변환 + * @param date + */ +export const formatTime = (date: Date): string => { + return date.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit", hour12: false }); +}; + +/** + * 이벤트 상태 반환 + * @param event + * @param currentTime past: 지난 이벤트, current: 현재 진행중인 이벤트, future: 미래 이벤트 + */ +export const getEventStatus = ( + event: timeTableInfoProps, + currentTime: Date, +): "past" | "current" | "future" => { + let adjustedStartTime = new Date(event.startTime); + + // KNU-ARTIST 항목에 대한 시작 시간 조정 + if (event.description.includes("KNU-ARTIST")) { + if (event.description.includes("댄스")) { + adjustedStartTime = new Date(adjustedStartTime.getTime() - 5 * 60000); + } else if (event.description.includes("밴드")) { + adjustedStartTime = new Date(adjustedStartTime.getTime() - 4 * 60000); + } + } + + if (currentTime < adjustedStartTime) return "future"; + if (currentTime > event.endTime) return "past"; + return "current"; +}; diff --git a/src/shared/routing/routerInfo.ts b/src/shared/routing/routerInfo.ts index 9094ba4..a1a2b63 100644 --- a/src/shared/routing/routerInfo.ts +++ b/src/shared/routing/routerInfo.ts @@ -79,7 +79,7 @@ export const routerInfo: routerInfoType[] = [ path: "timetable", element: lazy(() => import("../../pages/Timetable")), english: "Timetable", - korean: "타임테이블", + korean: "축제 일정", expose: true, mainPage: true, scrollOptions: "never", diff --git a/src/shared/styles/vars.css.ts b/src/shared/styles/vars.css.ts index 722b43c..21780a8 100644 --- a/src/shared/styles/vars.css.ts +++ b/src/shared/styles/vars.css.ts @@ -1,12 +1,14 @@ import { createGlobalTheme, fontFace } from "@vanilla-extract/css"; +import boldFont from "../../assets/fonts/subset-PyeongChangPeace-Bold.woff2"; +import lightFont from "../../assets/fonts/subset-PyeongChangPeace-Light.woff2"; const pyeongChangBold = fontFace({ - src: "url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2206-02@1.0/PyeongChangPeace-Bold.woff2') format('woff2');", + src: `url('${boldFont}') format('woff2');`, fontWeight: 700, fontStyle: "nomal", }); const pyeongChangLight = fontFace({ - src: "url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2206-02@1.0/PyeongChangPeace-Light.woff2') format('woff2');", + src: `url('${lightFont}') format('woff2');`, fontWeight: 300, fontStyle: "nomal", }); diff --git a/src/shared/types/timeTable.ts b/src/shared/types/timeTable.ts index 17a1224..fb52f03 100644 --- a/src/shared/types/timeTable.ts +++ b/src/shared/types/timeTable.ts @@ -8,6 +8,7 @@ export interface timeTableInfoProps { date: Date; startTime: Date; endTime: Date; + link?: { text: string; url: string }; } export interface timeTableFilterProps {