From 9833c23c9fc17d6f2f87196b64eb76b3e01db31c Mon Sep 17 00:00:00 2001 From: Clearsu Date: Tue, 12 Dec 2023 15:29:23 +0900 Subject: [PATCH 1/5] [Feat] TournamentPreview #1149 --- components/main/Section.tsx | 4 +- .../TournamentPreview/TournamentPreview.tsx | 81 +++++++++++++ .../TournamentPreviewItem.tsx | 45 ++++++++ components/tournament/TournamentMegaphone.tsx | 109 ------------------ .../TournamentPreview.module.scss | 13 +++ .../TournamentPreviewItem.module.scss | 35 ++++++ 6 files changed, 176 insertions(+), 111 deletions(-) create mode 100644 components/main/TournamentPreview/TournamentPreview.tsx create mode 100644 components/main/TournamentPreview/TournamentPreviewItem.tsx delete mode 100644 components/tournament/TournamentMegaphone.tsx create mode 100644 styles/main/TournamentPreview/TournamentPreview.module.scss create mode 100644 styles/main/TournamentPreview/TournamentPreviewItem.module.scss diff --git a/components/main/Section.tsx b/components/main/Section.tsx index 3f2fd3769..7081161f6 100644 --- a/components/main/Section.tsx +++ b/components/main/Section.tsx @@ -2,8 +2,8 @@ import { useRouter } from 'next/router'; import React from 'react'; import { FaChevronRight } from 'react-icons/fa'; import GameResult from 'components/game/GameResult'; +import TournamentPreview from 'components/main/TournamentPreview/TournamentPreview'; import RankListMain from 'components/rank/topRank/RankListMain'; -import TournamentMegaphone from 'components/tournament/TournamentMegaphone'; import styles from 'styles/main/Section.module.scss'; type SectionProps = { @@ -18,7 +18,7 @@ export default function Section({ sectionTitle, path }: SectionProps) { const pathCheck: pathType = { game: , rank: , - tournament: , + tournament: , }; return ( diff --git a/components/main/TournamentPreview/TournamentPreview.tsx b/components/main/TournamentPreview/TournamentPreview.tsx new file mode 100644 index 000000000..3c551050e --- /dev/null +++ b/components/main/TournamentPreview/TournamentPreview.tsx @@ -0,0 +1,81 @@ +import { useRouter } from 'next/router'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { useSetRecoilState } from 'recoil'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { instance } from 'utils/axios'; +import { errorState } from 'utils/recoil/error'; +import useInterval from 'hooks/useInterval'; +import styles from 'styles/main/TournamentPreview/TournamentPreview.module.scss'; +import TournamentPreviewItem from './TournamentPreviewItem'; + +export default function TournamentPreview() { + const [tournamentList, setTournamentList] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); + const virtuoso = useRef(null); + const router = useRouter(); + const setError = useSetRecoilState(errorState); + + const fetchTournamentList = useCallback(async () => { + try { + const liveRes = await instance.get( + '/pingpong/tournaments?page=1&status=LIVE' + ); + const beforeRes = await instance.get( + '/pingpong/tournaments?page=1&status=BEFORE' + ); + const joinedRes = [ + ...liveRes.data.tournaments, + ...beforeRes.data.tournaments, + ]; + setTournamentList(joinedRes); + } catch (e: any) { + setError('JC04'); + } + }, [setError]); + + useEffect(() => { + fetchTournamentList(); + }, [fetchTournamentList]); + + useInterval(() => { + if (tournamentList.length === 0) { + return; + } + const nextIndex = (selectedIndex + 1) % tournamentList.length; + setSelectedIndex(nextIndex); + if (virtuoso.current !== null) { + virtuoso.current.scrollToIndex({ + index: nextIndex, + align: 'start', + behavior: 'smooth', + }); + } + }, 5000); + + return ( +
router.push('tournament')} + > + {tournamentList.length > 0 && ( + ( + + )} + style={{ height: '100%' }} + /> + )} +
+ ); +} diff --git a/components/main/TournamentPreview/TournamentPreviewItem.tsx b/components/main/TournamentPreview/TournamentPreviewItem.tsx new file mode 100644 index 000000000..c00c2272d --- /dev/null +++ b/components/main/TournamentPreview/TournamentPreviewItem.tsx @@ -0,0 +1,45 @@ +import styles from 'styles/main/TournamentPreview/TournamentPreviewItem.module.scss'; + +interface TournamentPreviewItemProps { + id: number; + title: string; + startTime: string; + endTime: string; + status: string; +} + +export default function TournamentPreviewItem({ + title, + startTime, + endTime, + status, +}: TournamentPreviewItemProps) { + function formatTime(timeString: string) { + const date = new Date(timeString); + return date.toLocaleTimeString('ko-KR', { + year: 'numeric', + month: 'numeric', + hour: 'numeric', + minute: 'numeric', + day: 'numeric', + hour12: true, + }); + } + + const formattedStartTime = formatTime(startTime); + const formattedEndTime = formatTime(endTime); + + const statusColor = status === 'LIVE' ? 'live' : 'scheduled'; + + return ( +
+
+

{title}

+
+ {status === 'LIVE' ? '경기 중' : '예정'} +
+
+ ~ +
+ ); +} diff --git a/components/tournament/TournamentMegaphone.tsx b/components/tournament/TournamentMegaphone.tsx deleted file mode 100644 index d41a01515..000000000 --- a/components/tournament/TournamentMegaphone.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useRouter } from 'next/router'; -import { useState, useEffect, useRef } from 'react'; -import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; -import { TournamentInfo } from 'types/tournamentTypes'; -import { instance } from 'utils/axios'; -import { dateToString } from 'utils/handleTime'; -import { mockInstance } from 'utils/mockAxios'; -import useInterval from 'hooks/useInterval'; -import styles from 'styles/Layout/MegaPhone.module.scss'; - -interface IMegaphoneContent { - megaphoneId?: number; - content: string; - intraId: string; - onClick: (event: React.MouseEvent) => void; -} - -type MegaphoneList = Array; - -export const MegaphoneItem = ({ - content, - intraId, - onClick, -}: IMegaphoneContent) => { - return ( -
-
{intraId}
-
{content}
-
- ); -}; - -const TournamentMegaphone = () => { - const [contents, setContents] = useState([]); - const [TournamentList, setTournamentList] = useState([]); - const [megaphoneData, setMegaphoneData] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(0); - const virtuoso = useRef(null); - const router = useRouter(); - - function getTournamentListHandler() { - return mockInstance - .get(`tournament?size=20&page=1&status=BEFORE`) - .then((res) => { - setTournamentList(res.data.tournaments); - }); - } - - const goTournamentPage = (event: React.MouseEvent) => { - router.push('tournament'); - }; - - useEffect(() => { - if (contents.length === 0) getTournamentListHandler(); - }, [contents]); - - useEffect(() => { - if (contents.length > 0) setMegaphoneData([...contents]); - else { - setMegaphoneData([ - ...TournamentList.map( - (item): IMegaphoneContent => ({ - megaphoneId: item.tournamentId, - content: item.title, - intraId: dateToString(new Date(item.startTime)), - onClick: goTournamentPage, - }) - ), - ]); - } - }, [contents, TournamentList]); - - useInterval(() => { - if (!megaphoneData) return; - const nextIndex = (selectedIndex + 1) % megaphoneData.length; - setSelectedIndex(nextIndex); - if (virtuoso.current !== null) - virtuoso.current.scrollToIndex({ - index: nextIndex, - align: 'start', - behavior: 'smooth', - }); - }, 4000); - - return ( -
-
- {!!megaphoneData && megaphoneData.length > 0 && ( - ( - - )} - style={{ height: '100%' }} - /> - )} -
-
- ); -}; - -export default TournamentMegaphone; diff --git a/styles/main/TournamentPreview/TournamentPreview.module.scss b/styles/main/TournamentPreview/TournamentPreview.module.scss new file mode 100644 index 000000000..6a8c05d39 --- /dev/null +++ b/styles/main/TournamentPreview/TournamentPreview.module.scss @@ -0,0 +1,13 @@ +.rollingBanner { + width: 100%; + height: 5rem; + color: white; + background: #8034f7; + border-radius: 1rem; +} + +.virtuoso { + // NOTE : 라이브러리에서 인라인 스타일로 overflow-y: auto를 주고 있어 스크롤바가 생김 + // 이를 상쇄하기 위해서 !important를 사용함. + overflow-y: hidden !important; +} diff --git a/styles/main/TournamentPreview/TournamentPreviewItem.module.scss b/styles/main/TournamentPreview/TournamentPreviewItem.module.scss new file mode 100644 index 000000000..8da1ac0f9 --- /dev/null +++ b/styles/main/TournamentPreview/TournamentPreviewItem.module.scss @@ -0,0 +1,35 @@ +.itemWrapper { + width: 100%; + height: 5rem; + padding: 1rem; + + .titleStatusWrapper { + display: flex; + align-items: center; + + h4 { + margin: 0; + } + + .statusWrapper { + display: flex; + padding: 0.3rem; + margin-left: 0.5rem; + font-size: 0.7rem; + border-radius: 0.5rem; + align-items: center; + + &.live { + background-color: red; + } + &.scheduled { + background-color: green; + } + } + } + + time { + font-size: 0.8rem; + color: yellow; + } +} From c2ea059d34fd8b5d334b9b2ae12c20aeac05f379 Mon Sep 17 00:00:00 2001 From: Clearsu Date: Tue, 12 Dec 2023 15:51:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[Refactor]=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?div=20=EC=82=AD=EC=A0=9C=20#1149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/index.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pages/index.tsx b/pages/index.tsx index 9ceb0d2c3..83f7b2e34 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,18 +6,10 @@ import styles from 'styles/main/Home.module.scss'; const Home: NextPage = () => { return (
-
- -
-
-
-
-
-
-
-
-
-
+ +
+
+
); }; From 0c6d3236b98a3f9ab4b05923d0a95e94e03c912b Mon Sep 17 00:00:00 2001 From: Clearsu Date: Tue, 12 Dec 2023 21:04:36 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[Feat]=20=EC=A7=84=ED=96=89=EC=A4=91=20?= =?UTF-8?q?=ED=98=B9=EC=9D=80=20=EC=98=88=EC=A0=95=EC=9D=B8=20=ED=86=A0?= =?UTF-8?q?=EB=84=88=EB=A8=BC=ED=8A=B8=20=EA=B2=BD=EA=B8=B0=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=EC=97=90=EB=A7=8C=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=ED=91=9C=EC=8B=9C=20#1149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pages/index.tsx b/pages/index.tsx index 83f7b2e34..eabfdfd17 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,13 +1,18 @@ import type { NextPage } from 'next'; import SearchBar from 'components/main/SearchBar'; import Section from 'components/main/Section'; +import useBeforeLiveTournamentData from 'hooks/tournament/useBeforeLiveTournamentData'; import styles from 'styles/main/Home.module.scss'; const Home: NextPage = () => { + const tournamentData = useBeforeLiveTournamentData(); + return (
-
+ {tournamentData && ( +
+ )}
From 2bdadfe6146c5d6aac7e10ae3f77940f6c6ace2f Mon Sep 17 00:00:00 2001 From: Clearsu Date: Tue, 12 Dec 2023 21:05:24 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[Refactor]=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=ED=9B=85=20=EB=B6=84=EB=A6=AC=20#1149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TournamentPreview/TournamentPreview.tsx | 51 +++++-------------- .../tournament/useBeforeLiveTournamentData.ts | 35 +++++++++++++ 2 files changed, 48 insertions(+), 38 deletions(-) create mode 100644 hooks/tournament/useBeforeLiveTournamentData.ts diff --git a/components/main/TournamentPreview/TournamentPreview.tsx b/components/main/TournamentPreview/TournamentPreview.tsx index 3c551050e..d642c17cf 100644 --- a/components/main/TournamentPreview/TournamentPreview.tsx +++ b/components/main/TournamentPreview/TournamentPreview.tsx @@ -1,48 +1,23 @@ import { useRouter } from 'next/router'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useRef } from 'react'; import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; -import { useSetRecoilState } from 'recoil'; import { TournamentInfo } from 'types/tournamentTypes'; -import { instance } from 'utils/axios'; -import { errorState } from 'utils/recoil/error'; +import useBeforeLiveTournamentData from 'hooks/tournament/useBeforeLiveTournamentData'; import useInterval from 'hooks/useInterval'; import styles from 'styles/main/TournamentPreview/TournamentPreview.module.scss'; import TournamentPreviewItem from './TournamentPreviewItem'; export default function TournamentPreview() { - const [tournamentList, setTournamentList] = useState([]); + const data: TournamentInfo[] | undefined = useBeforeLiveTournamentData(); const [selectedIndex, setSelectedIndex] = useState(0); const virtuoso = useRef(null); const router = useRouter(); - const setError = useSetRecoilState(errorState); - - const fetchTournamentList = useCallback(async () => { - try { - const liveRes = await instance.get( - '/pingpong/tournaments?page=1&status=LIVE' - ); - const beforeRes = await instance.get( - '/pingpong/tournaments?page=1&status=BEFORE' - ); - const joinedRes = [ - ...liveRes.data.tournaments, - ...beforeRes.data.tournaments, - ]; - setTournamentList(joinedRes); - } catch (e: any) { - setError('JC04'); - } - }, [setError]); - - useEffect(() => { - fetchTournamentList(); - }, [fetchTournamentList]); useInterval(() => { - if (tournamentList.length === 0) { + if (!data || data?.length === 0) { return; } - const nextIndex = (selectedIndex + 1) % tournamentList.length; + const nextIndex = (selectedIndex + 1) % data.length; setSelectedIndex(nextIndex); if (virtuoso.current !== null) { virtuoso.current.scrollToIndex({ @@ -58,19 +33,19 @@ export default function TournamentPreview() { className={styles.rollingBanner} onClick={() => router.push('tournament')} > - {tournamentList.length > 0 && ( + {data && data.length > 0 && ( ( )} style={{ height: '100%' }} diff --git a/hooks/tournament/useBeforeLiveTournamentData.ts b/hooks/tournament/useBeforeLiveTournamentData.ts new file mode 100644 index 000000000..338da1a01 --- /dev/null +++ b/hooks/tournament/useBeforeLiveTournamentData.ts @@ -0,0 +1,35 @@ +import { useCallback } from 'react'; +import { useQuery } from 'react-query'; +import { useSetRecoilState } from 'recoil'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { instance } from 'utils/axios'; +import { errorState } from 'utils/recoil/error'; + +export default function useBeforeLiveTournamentData() { + const setError = useSetRecoilState(errorState); + const fetchTournamentData = useCallback(async () => { + const liveRes = await instance.get( + '/pingpong/tournaments?page=1&status=LIVE' + ); + const beforeRes = await instance.get( + '/pingpong/tournaments?page=1&status=BEFORE' + ); + const combinedData = [ + ...liveRes.data.tournaments, + ...beforeRes.data.tournaments, + ]; + return combinedData; + }, []); + + const { data, isError } = useQuery( + 'beforeLiveTournamentData', + fetchTournamentData, + { retry: 1, staleTime: 60000 } + ); + + if (isError) { + setError('JC04'); + } + + return data; +} From 80d0354727a855baa8d8f2e4f37d8b3568d81af6 Mon Sep 17 00:00:00 2001 From: Clearsu Date: Wed, 13 Dec 2023 14:39:20 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[Refactor]=20ko-KR=20locale=20time=20string?= =?UTF-8?q?=20=EB=B0=98=ED=99=98=20=ED=95=A8=EC=88=98=20util=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20#1149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TournamentPreview/TournamentPreviewItem.tsx | 17 +++-------------- utils/handleTime.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/components/main/TournamentPreview/TournamentPreviewItem.tsx b/components/main/TournamentPreview/TournamentPreviewItem.tsx index c00c2272d..174f9f06e 100644 --- a/components/main/TournamentPreview/TournamentPreviewItem.tsx +++ b/components/main/TournamentPreview/TournamentPreviewItem.tsx @@ -1,3 +1,4 @@ +import { dateToKRLocaleTimeString } from 'utils/handleTime'; import styles from 'styles/main/TournamentPreview/TournamentPreviewItem.module.scss'; interface TournamentPreviewItemProps { @@ -14,20 +15,8 @@ export default function TournamentPreviewItem({ endTime, status, }: TournamentPreviewItemProps) { - function formatTime(timeString: string) { - const date = new Date(timeString); - return date.toLocaleTimeString('ko-KR', { - year: 'numeric', - month: 'numeric', - hour: 'numeric', - minute: 'numeric', - day: 'numeric', - hour12: true, - }); - } - - const formattedStartTime = formatTime(startTime); - const formattedEndTime = formatTime(endTime); + const formattedStartTime = dateToKRLocaleTimeString(new Date(startTime)); + const formattedEndTime = dateToKRLocaleTimeString(new Date(endTime)); const statusColor = status === 'LIVE' ? 'live' : 'scheduled'; diff --git a/utils/handleTime.ts b/utils/handleTime.ts index 1a78b1afe..77e704102 100644 --- a/utils/handleTime.ts +++ b/utils/handleTime.ts @@ -160,3 +160,19 @@ export const dateToStringShort = (d: Date) => { const min = d.getMinutes().toString().padStart(2, '0'); return `${year}-${month}-${date} ${hour}:${min}`; }; + +/** + * 시간을 YYYY-MM-DD 오후/오전 HH:MM 형식으로 반환 + * @param {Date} d + * @return 시간 문자열(YYYY-MM-DD 오후/오전 HH:MM) + */ +export const dateToKRLocaleTimeString = (d: Date) => { + return d.toLocaleTimeString('ko-KR', { + year: 'numeric', + month: 'numeric', + hour: 'numeric', + minute: 'numeric', + day: 'numeric', + hour12: true, + }); +};