diff --git a/frontend/src/components/CompetitionDetail/AfterCompetition.tsx b/frontend/src/components/CompetitionDetail/AfterCompetition.tsx new file mode 100644 index 0000000..3be0e66 --- /dev/null +++ b/frontend/src/components/CompetitionDetail/AfterCompetition.tsx @@ -0,0 +1,38 @@ +import { css } from '@style/css'; + +import { CompetitionInfo } from '@/apis/competitions'; + +import CompetitionDetailInfo from './CompetitionDetailInfo'; +import ProblemList from './ProblemList'; + +interface Props { + competitionId: number; + competition: CompetitionInfo; + competitionSchedule: string; +} + +const AFTER_COMPETITION_TEXT = ' 종료'; + +export default function AfterCompetition({ + competitionId, + competition, + competitionSchedule, +}: Props) { + return ( +
+ + +
+ ); +} + +const containerStyle = css({ + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + border: '1px solid #ccc', +}); diff --git a/frontend/src/components/CompetitionDetail/BeforeCompetition.tsx b/frontend/src/components/CompetitionDetail/BeforeCompetition.tsx new file mode 100644 index 0000000..a129b0a --- /dev/null +++ b/frontend/src/components/CompetitionDetail/BeforeCompetition.tsx @@ -0,0 +1,51 @@ +import { css } from '@style/css'; + +import { CompetitionInfo } from '@/apis/competitions'; + +import JoinCompetitionButton from '../Main/Buttons/JoinCompetitionButton'; +import EnterCompetitionButton from './Buttons/EnterCompetitionButton'; +import CompetitionDetailInfo from './CompetitionDetailInfo'; + +interface Props { + competitionId: number; + competition: CompetitionInfo; + startsAt: Date; + endsAt: Date; + competitionSchedule: string; +} + +export default function BeforeCompetition({ + competitionId, + competition, + startsAt, + endsAt, + competitionSchedule, +}: Props) { + const BEFORE_COMPETITION_TEXT = ` 시작 전`; + + return ( +
+ +
+ + +
+
+ ); +} + +const containerStyle = css({ + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + border: '1px solid #ccc', +}); + +const buttonContainerStyle = css({ + display: 'flex', + gap: '16px', +}); diff --git a/frontend/src/components/CompetitionDetail/Buttons/EnterCompetitionButton.tsx b/frontend/src/components/CompetitionDetail/Buttons/EnterCompetitionButton.tsx new file mode 100644 index 0000000..0512dba --- /dev/null +++ b/frontend/src/components/CompetitionDetail/Buttons/EnterCompetitionButton.tsx @@ -0,0 +1,34 @@ +import { useNavigate } from 'react-router-dom'; + +import useAuth from '@/hooks/login/useAuth'; + +interface Props { + id: number; + startsAt: Date; + endsAt: Date; +} + +export default function EnterCompetitionButton({ id, startsAt, endsAt }: Props) { + const competitionLink = `/contest/${id}`; + const { isLoggedin } = useAuth(); + const navigate = useNavigate(); + + const handleNavigate = () => { + const currentTime = new Date(); + + if (!isLoggedin) { + alert('로그인이 필요합니다.'); + navigate('/login'); + } else if (currentTime < startsAt) { + alert('아직 대회가 시작되지 않았습니다. 다시 시도해주세요'); + window.location.reload(); + } else if (currentTime >= endsAt) { + alert('해당 대회는 종료되었습니다.'); + window.location.reload(); + } else { + navigate(competitionLink); + } + }; + + return ; +} diff --git a/frontend/src/components/CompetitionDetail/CompetitionDetailContent.tsx b/frontend/src/components/CompetitionDetail/CompetitionDetailContent.tsx new file mode 100644 index 0000000..da83829 --- /dev/null +++ b/frontend/src/components/CompetitionDetail/CompetitionDetailContent.tsx @@ -0,0 +1,39 @@ +// CompetitionDetailContent.js +import { CompetitionInfo } from '@/apis/competitions'; +import AfterCompetition from '@/components/CompetitionDetail/AfterCompetition'; +import BeforeCompetition from '@/components/CompetitionDetail/BeforeCompetition'; +import DuringCompetition from '@/components/CompetitionDetail/DuringCompetition'; + +interface Props { + competitionId: number; + competition: CompetitionInfo; + startsAt: Date; + endsAt: Date; + competitionSchedule: string; +} + +export function CompetitionDetailContent({ + competitionId, + competition, + startsAt, + endsAt, + competitionSchedule, +}: Props) { + const currentDate = new Date(); + + if (currentDate < startsAt) { + return ( + + ); + } else if (currentDate < endsAt) { + return ( + + ); + } else { + return ; + } +} diff --git a/frontend/src/components/CompetitionDetail/CompetitionDetailInfo.tsx b/frontend/src/components/CompetitionDetail/CompetitionDetailInfo.tsx new file mode 100644 index 0000000..92582d8 --- /dev/null +++ b/frontend/src/components/CompetitionDetail/CompetitionDetailInfo.tsx @@ -0,0 +1,46 @@ +import { css } from '@style/css'; + +import { CompetitionInfo } from '@/apis/competitions'; + +interface Props { + competition: CompetitionInfo; + text: string; + competitionSchedule: string; +} + +export default function CompetitionDetailInfo({ competition, text, competitionSchedule }: Props) { + return ( +
+
+ {competition.name} + {text} +
+ {competitionSchedule} +
{competition.detail}
+
+ ); +} + +const infoContainerStyle = css({ + display: 'flex', + flexDirection: 'column', +}); + +const competitionNameStyle = css({ + fontSize: '18px', + fontWeight: 'bold', + color: 'black', + marginBottom: '8px', +}); + +const statusTextStyle = css({ + fontSize: '12px', + color: 'gray', + marginBottom: '8px', +}); + +const additionalTextStyle = css({ + fontSize: '14px', + color: 'black', + marginBottom: '8px', +}); diff --git a/frontend/src/components/CompetitionDetail/DuringCompetition.tsx b/frontend/src/components/CompetitionDetail/DuringCompetition.tsx new file mode 100644 index 0000000..5fbbcd6 --- /dev/null +++ b/frontend/src/components/CompetitionDetail/DuringCompetition.tsx @@ -0,0 +1,42 @@ +import { css } from '@style/css'; + +import { CompetitionInfo } from '@/apis/competitions'; + +import EnterCompetitionButton from './Buttons/EnterCompetitionButton'; +import CompetitionDetailInfo from './CompetitionDetailInfo'; + +interface Props { + competitionId: number; + competition: CompetitionInfo; + startsAt: Date; + endsAt: Date; + competitionSchedule: string; +} + +const DURING_COMPETITION_TEXT = ' 진행중'; + +export default function DuringCompetition({ + competitionId, + competition, + startsAt, + endsAt, + competitionSchedule, +}: Props) { + return ( +
+ + +
+ ); +} + +const containerStyle = css({ + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + border: '1px solid #ccc', +}); diff --git a/frontend/src/components/CompetitionDetail/ProblemList.tsx b/frontend/src/components/CompetitionDetail/ProblemList.tsx new file mode 100644 index 0000000..5fbb763 --- /dev/null +++ b/frontend/src/components/CompetitionDetail/ProblemList.tsx @@ -0,0 +1,34 @@ +import { Link } from 'react-router-dom'; + +import { useCompetitionProblemList } from '@/hooks/problem'; + +interface Props { + competitionId: number; +} + +export default function ProblemList({ competitionId }: Props) { + const { problemList } = useCompetitionProblemList(competitionId); + + return ( +
+ + + + + + + + + {problemList.map((problem) => ( + + + + + ))} + +
번호문제 제목
{problem.id} + {problem.title} +
+
+ ); +} diff --git a/frontend/src/pages/CompetitionDetailPage.tsx b/frontend/src/pages/CompetitionDetailPage.tsx new file mode 100644 index 0000000..8a4ad72 --- /dev/null +++ b/frontend/src/pages/CompetitionDetailPage.tsx @@ -0,0 +1,32 @@ +import { useParams } from 'react-router-dom'; + +import { CompetitionDetailContent } from '@/components/CompetitionDetail/CompetitionDetailContent'; +import Header from '@/components/Header'; +import { useCompetition } from '@/hooks/competition'; +import { formatDate } from '@/utils/date'; + +export default function CompetitionDetailPage() { + const { id } = useParams<{ id: string }>(); + const competitionId: number = id ? parseInt(id, 10) : -1; + const { competition } = useCompetition(competitionId); + // 대회 상태에 따른 페이지를 구성하기 위해 현재 날짜, 시작 시간, 종료 시간을 가져옴 + const startsAt = new Date(competition?.startsAt || ''); + const endsAt = new Date(competition?.endsAt || ''); + const formattedStartsAt = formatDate(startsAt, 'YYYY. MM. DD. hh:mm'); + const formattedEndsAt = formatDate(endsAt, 'YYYY. MM. DD. hh:mm'); + + const competitionSchedule = `시작: ${formattedStartsAt} 종료: ${formattedEndsAt}`; + + return ( +
+
+ +
+ ); +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index be13f57..bdff5bd 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,6 +7,7 @@ import MainPage from '@/pages/MainPage'; import ProblemPage from '@/pages/ProblemPage'; import App from './App'; +import CompetitionDetailPage from './pages/CompetitionDetailPage'; const router = createBrowserRouter([ { @@ -30,6 +31,10 @@ const router = createBrowserRouter([ element: , }, { path: '/login', element: }, + { + path: '/contest/detail/:id', + element: , + }, ], }, ]); diff --git a/frontend/src/utils/date/index.ts b/frontend/src/utils/date/index.ts index 96ed403..8b01e7a 100644 --- a/frontend/src/utils/date/index.ts +++ b/frontend/src/utils/date/index.ts @@ -16,6 +16,17 @@ export const formatDate = (date: Date, form: string) => { return date.toISOString().slice(0, 'YYYY-MM-DDThh:mm'.length); } + if (form === 'YYYY. MM. DD. hh:mm') { + return date.toLocaleString('ko-KR', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: false, + }); + } + return ''; };