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 '';
};