diff --git a/src/pages/Timetable/.css.ts b/src/pages/Timetable/.css.ts index cfac435..b46ade2 100644 --- a/src/pages/Timetable/.css.ts +++ b/src/pages/Timetable/.css.ts @@ -17,16 +17,30 @@ export const section = style({ }); const baseFilterButton = style({ + position: "relative", padding: "10px 15px", margin: "0 5px 10px 0", border: "none", borderRadius: "20px", cursor: "pointer", - transition: "background-color 0.3s ease", - backgroundColor: "rgba(79, 205, 197, 0.5)", + transition: "all 0.3s ease", + overflow: "hidden", + + "::before": { + content: '""', + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(255, 255, 255, 0.5)", + transition: "background-color 0.3s ease", + zIndex: -1, + }, + "@media": { "screen and (max-width: 768px)": { - padding: "16px 26px", + padding: "8px 12px", margin: "0 3px 8px 0", }, }, @@ -37,7 +51,8 @@ export const filterButton = styleVariants({ baseFilterButton, { color: vars.color.white, - ":hover": { + + ":hover::before": { backgroundColor: "rgba(255, 255, 255, 0.2)", }, }, @@ -45,26 +60,17 @@ export const filterButton = styleVariants({ active: [ baseFilterButton, { + backgroundImage: "linear-gradient(to top left, #04D1C3, #009efd, #FFFFFF)", fontWeight: "bold", - backgroundColor: "rgba(255, 255, 255, 0.3)", // 강조 네모 배경 - color: "transparent", // 텍스트 색상 투명 - backgroundImage: "linear-gradient(to top left, #04D1C3,#009efd, #FFFFFF)", // 텍스트 그라데이션 - WebkitBackgroundClip: "text", // 텍스트에만 그라데이션 적용 - WebkitTextFillColor: "transparent", // 텍스트 색상 채우기 제거 - position: "relative", // 텍스트와 배경 분리 + WebkitBackgroundClip: "text", + backgroundClip: "text", + color: "transparent", + "::before": { - // 배경 유지 - content: '""', - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(255, 255, 255, 0.3)", // 강조 네모 배경 - borderRadius: "20px", // 배경의 모서리 둥글게 - zIndex: -1, // 배경이 텍스트 뒤로 가게 + backgroundColor: "rgba(255, 255, 255, 0.5)", }, - ":hover": { + + ":hover::before": { backgroundColor: "rgba(255, 255, 255, 0.4)", }, }, @@ -180,6 +186,7 @@ export const artistImage = style({ export const artistName = style({ fontSize: "1.7rem", fontFamily: vars.font.pyeongChangLight, + wordBreak: "keep-all", "@media": { "screen and (max-width: 768px)": { fontSize: "1.7rem", diff --git a/src/pages/Timetable/index.tsx b/src/pages/Timetable/index.tsx index 2d8a65a..0adfc63 100644 --- a/src/pages/Timetable/index.tsx +++ b/src/pages/Timetable/index.tsx @@ -1,118 +1,20 @@ -import React, { useEffect, useState, useMemo, useCallback, useRef } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { timeTableInfo } from "./timeTableInfo.ts"; -import { timeTableFilterProps, timeTableInfoProps } from "../../shared/types/timeTable.ts"; import * as styles from "./.css.ts"; -import { Link } from "react-router-dom"; -import { artistInfoListProps } from "../../shared/types/mainPage.ts"; +import { FilterButton, TimeTableItem } from "./subComponents.tsx"; +import { clearTime, END_DATE, START_DATE, TIME_TABLE_FILTER } from "./utils.tsx"; -// Constants -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") }, -]; - -const START_DATE = new Date("2024-09-23T00:00:00+09:00"); -const END_DATE = new Date("2024-09-26T00:00:00+09:00"); - -const clearTime = (date: Date): Date => { - return new Date(date.getFullYear(), date.getMonth(), date.getDate()); -}; - -const formatTime = (date: Date): string => { - return date.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit", hour12: false }); -}; - -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"; -}; - -// Sub-components -const FilterButton: React.FC<{ - timeTable: timeTableFilterProps; - onClick: (date: Date) => void; - isActive: boolean; -}> = React.memo(({ timeTable, onClick, isActive }) => ( - -)); - -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.link && ( - - {timeTable.link.text} - - )} -

- )} -

- {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); @@ -132,6 +34,11 @@ export default function Timetable() { return () => clearInterval(timer); }, []); + /** + * 필터링된 타임테이블 정보 + * @see timeTableInfo + * @see useMemo + */ const filteredTimeTableInfo = useMemo( () => timeTableInfo @@ -140,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) => ( @@ -181,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 7d76ec1..aa88aea 100644 --- a/src/pages/Timetable/timeTableInfo.ts +++ b/src/pages/Timetable/timeTableInfo.ts @@ -387,8 +387,8 @@ 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"), @@ -399,16 +399,16 @@ export const timeTableInfo: timeTableInfoProps[] = [ }, }, { - 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"), @@ -419,16 +419,16 @@ export const timeTableInfo: timeTableInfoProps[] = [ }, }, { - 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"), @@ -439,9 +439,9 @@ export const timeTableInfo: timeTableInfoProps[] = [ }, }, { - 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"), 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",