From b68ab55edc9acda0233a8c5cb099adefd13d3c94 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 19:26:48 +0900 Subject: [PATCH 01/24] =?UTF-8?q?feat:=20=EB=8C=80=ED=9A=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/CompetitionCreatePage.tsx | 167 ++++++++++++++++++ frontend/src/router.tsx | 5 + .../utils/date/__tests__/formatDate.spec.ts | 18 ++ .../utils/date/__tests__/toLocalDate.spec.ts | 19 ++ frontend/src/utils/date/index.ts | 17 ++ 5 files changed, 226 insertions(+) create mode 100644 frontend/src/pages/CompetitionCreatePage.tsx create mode 100644 frontend/src/utils/date/__tests__/formatDate.spec.ts create mode 100644 frontend/src/utils/date/__tests__/toLocalDate.spec.ts create mode 100644 frontend/src/utils/date/index.ts diff --git a/frontend/src/pages/CompetitionCreatePage.tsx b/frontend/src/pages/CompetitionCreatePage.tsx new file mode 100644 index 0000000..17b7356 --- /dev/null +++ b/frontend/src/pages/CompetitionCreatePage.tsx @@ -0,0 +1,167 @@ +import { css } from '@style/css'; + +import type { ChangeEvent, HTMLAttributes } from 'react'; +import { useState } from 'react'; + +import { formatDate, toLocalDate } from '@/utils/date'; + +import axios from 'axios'; + +export default function CompetitionCreatePage() { + const [name, setName] = useState(''); + const [detail, setDetail] = useState(''); + const [maxParticipants, setMaxParticipants] = useState(0); + + const currentDate = toLocalDate(new Date()); + const currentDateStr = formatDate(currentDate, 'YYYY-MM-DDThh:mm'); + + const [startsAt, setStartsAt] = useState(currentDateStr); + const [endsAt, setEndsAt] = useState(currentDateStr); + + function handleChangeName(e: ChangeEvent) { + const newName = e.target.value; + setName(newName); + } + + function handleChangeDetail(e: ChangeEvent) { + const newDetail = e.target.value; + setDetail(newDetail); + } + + function handleChangeMaxParticipants(e: ChangeEvent) { + const newMaxParticipants = Number(e.target.value); + setMaxParticipants(newMaxParticipants); + } + + function handleChangeStartsAt(e: ChangeEvent) { + const newStartsAt = e.target.value; + setStartsAt(newStartsAt); + } + + function handleChangeEndsAt(e: ChangeEvent) { + const newEndsAt = e.target.value; + setEndsAt(newEndsAt); + } + + const handleSubmitCompetition = async () => { + // TODO: problems추가하기 + const data = { + name, + detail, + maxParticipants, + startsAt: new Date(startsAt).toISOString(), + endsAt: new Date(endsAt).toISOString(), + }; + await axios.post('http://101.101.208.240:3000/competitions', data); + }; + + return ( +
+

대회 생성

+
+ + + + + +
+ +
+ ); +} + +interface LabelProps extends HTMLAttributes {} + +const Label = ({ className, children, ...rest }: LabelProps) => { + return ( + + ); +}; + +interface LabelTextProps extends HTMLAttributes {} + +Label.Text = ({ className, children, ...rest }: LabelTextProps) => { + return ( + + {children} + + ); +}; + +const fieldSetStyle = css({ + display: 'flex', + flexDirection: 'column', +}); + +const inputStyle = css({ + border: '1px solid black', + width: '20rem', +}); + +const labelStyle = css({ + display: 'flex', + flexDirection: 'row', + textAlign: 'center', + gap: '4', +}); + +const labelTextStyle = css({ + margin: 'auto 0', + width: '8rem', + textAlign: 'left', +}); diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index ebd142c..5ce1d6e 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,5 +1,6 @@ import { createBrowserRouter } from 'react-router-dom'; +import CompetitionCreatePage from '@/pages/CompetitionCreatePage'; import ContestPage from '@/pages/ContestPage'; import ProblemPage from '@/pages/ProblemPage'; @@ -18,6 +19,10 @@ const router = createBrowserRouter([ path: '/problem/:id', // TODO: api 연동 후 수정 element: , }, + { + path: '/competition/create', + element: , + }, ], }, ]); diff --git a/frontend/src/utils/date/__tests__/formatDate.spec.ts b/frontend/src/utils/date/__tests__/formatDate.spec.ts new file mode 100644 index 0000000..ee5c781 --- /dev/null +++ b/frontend/src/utils/date/__tests__/formatDate.spec.ts @@ -0,0 +1,18 @@ +import { formatDate } from '../index'; +import { describe, expect, it } from 'vitest'; + +describe('formatDate', () => { + it('YYYY-MM-DDThh:mm 형식의 문자열을 반환한다.', () => { + const now = new Date(2000, 1, 1, 13, 1, 1); + const dateStr = formatDate(now, 'YYYY-MM-DDThh:mm'); + + expect(dateStr).toBe('2000-02-01T04:01'); + }); + + it('일치하는 형식이 없으면 빈 문자열을 반환한다.', () => { + const now = new Date(2000, 1, 1, 13, 1, 1); + const dateStr = formatDate(now, '??'); + + expect(dateStr).toBe(''); + }); +}); diff --git a/frontend/src/utils/date/__tests__/toLocalDate.spec.ts b/frontend/src/utils/date/__tests__/toLocalDate.spec.ts new file mode 100644 index 0000000..d5b8f10 --- /dev/null +++ b/frontend/src/utils/date/__tests__/toLocalDate.spec.ts @@ -0,0 +1,19 @@ +import { toLocalDate } from '../index'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('toLocalDate', () => { + beforeEach(() => { + const spyFn = vi.spyOn(Date.prototype, 'getTimezoneOffset'); + spyFn.mockReturnValue(-9 * 60); // kr 시간 차이만큼 timezoneOffset 설정 + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('입력받은 Date를 현재 지역 기준 Date로 변환한다.', () => { + const date = new Date(2000, 1, 1, 13, 1, 1); // 2000년 2월 1일 13시 1분 1초 + const localDate = toLocalDate(date); + // ISOString이 9시간 차이나는 것이 아니라 설정한 시간으로 나오게 됨 + expect(localDate.toISOString()).toBe('2000-02-01T13:01:01.000Z'); + }); +}); diff --git a/frontend/src/utils/date/index.ts b/frontend/src/utils/date/index.ts new file mode 100644 index 0000000..367eb7a --- /dev/null +++ b/frontend/src/utils/date/index.ts @@ -0,0 +1,17 @@ +const ONE_SEC_BY_MS = 1_000; +const ONE_MIN_BY_MS = 60 * ONE_SEC_BY_MS; + +export function toLocalDate(date: Date) { + const localTimeOffset = date.getTimezoneOffset() * ONE_MIN_BY_MS; + const localDate = new Date(date.getTime() - localTimeOffset); + + return localDate; +} + +export const formatDate = (date: Date, form: string) => { + if (form === 'YYYY-MM-DDThh:mm') { + return date.toISOString().slice(0, 'YYYY-MM-DDThh:mm'.length); + } + + return ''; +}; From d8a3cb6714e785c2cd95d56463b92d7fd9746f70 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 21:02:08 +0900 Subject: [PATCH 02/24] =?UTF-8?q?feat:=20=EB=AC=B8=EC=A0=9C=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EA=B8=B0=EB=8A=A5=20&=20=EB=8C=80=ED=9A=8C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문제를 선택할 수 있도록 기능을 추가함 - 대회 생성시 detail 페이지로 이동하도록 기능 추가 --- frontend/src/pages/CompetitionCreatePage.tsx | 67 ++++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/CompetitionCreatePage.tsx b/frontend/src/pages/CompetitionCreatePage.tsx index 17b7356..0eac81d 100644 --- a/frontend/src/pages/CompetitionCreatePage.tsx +++ b/frontend/src/pages/CompetitionCreatePage.tsx @@ -1,13 +1,21 @@ import { css } from '@style/css'; -import type { ChangeEvent, HTMLAttributes } from 'react'; -import { useState } from 'react'; +import type { ChangeEvent, HTMLAttributes, MouseEvent } from 'react'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { formatDate, toLocalDate } from '@/utils/date'; import axios from 'axios'; +type ProblemId = number; +type ProblemInfo = { + id: ProblemId; + title: string; +}; + export default function CompetitionCreatePage() { + const navigate = useNavigate(); const [name, setName] = useState(''); const [detail, setDetail] = useState(''); const [maxParticipants, setMaxParticipants] = useState(0); @@ -17,6 +25,19 @@ export default function CompetitionCreatePage() { const [startsAt, setStartsAt] = useState(currentDateStr); const [endsAt, setEndsAt] = useState(currentDateStr); + const [pickedProblemIds, setPickedProblemIds] = useState([]); + const [allProblems, setAllProblems] = useState([]); + + useEffect(() => { + async function fetchProblemList() { + const res = await axios.get('http://101.101.208.240:3000/problems'); + const problems = res.data; + + setAllProblems(problems); + } + + fetchProblemList(); + }, []); function handleChangeName(e: ChangeEvent) { const newName = e.target.value; @@ -43,21 +64,36 @@ export default function CompetitionCreatePage() { setEndsAt(newEndsAt); } - const handleSubmitCompetition = async () => { - // TODO: problems추가하기 + function handleSelectProblem(e: MouseEvent) { + const $target = (e.target as HTMLUListElement).closest('li'); + if (!$target) return; + + const problemId = Number($target.dataset['problemId']); + + if (pickedProblemIds.includes(problemId)) { + setPickedProblemIds((ids) => ids.filter((id) => id !== problemId)); + } else { + setPickedProblemIds((ids) => [...ids, problemId]); + } + } + + async function handleSubmitCompetition() { const data = { name, detail, maxParticipants, startsAt: new Date(startsAt).toISOString(), endsAt: new Date(endsAt).toISOString(), + problems: pickedProblemIds, }; - await axios.post('http://101.101.208.240:3000/competitions', data); - }; + const res = await axios.post('http://101.101.208.240:3000/competitions', data); + const { id } = res.data; + + navigate(`/contest/detail/${id}`); + } return (
-

대회 생성

+
    + {allProblems.map(({ id, title }) => ( +
  • + + {id}: {title} + + {pickedProblemIds.includes(id) ? : } +
  • + ))} +
+
+
    + {pickedProblemIds.map((problemId) => ( +
  • {problemId}
  • + ))} +
+
+
); } From 2560541f86792f73a6903e88c9ba42876e9a1a7f Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 22:42:17 +0900 Subject: [PATCH 10/24] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompetitionCreatePage -> CreateCompetitionPage (영문법에 맞춰 수정함) --- .../{CompetitionCreatePage.tsx => CreateCompetitionPage.tsx} | 0 frontend/src/router.tsx | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/pages/{CompetitionCreatePage.tsx => CreateCompetitionPage.tsx} (100%) diff --git a/frontend/src/pages/CompetitionCreatePage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx similarity index 100% rename from frontend/src/pages/CompetitionCreatePage.tsx rename to frontend/src/pages/CreateCompetitionPage.tsx diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 5ce1d6e..de0611f 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,7 +1,7 @@ import { createBrowserRouter } from 'react-router-dom'; -import CompetitionCreatePage from '@/pages/CompetitionCreatePage'; import ContestPage from '@/pages/ContestPage'; +import CreateCompetitionPage from '@/pages/CreateCompetitionPage'; import ProblemPage from '@/pages/ProblemPage'; import App from './App'; @@ -21,7 +21,7 @@ const router = createBrowserRouter([ }, { path: '/competition/create', - element: , + element: , }, ], }, From db0bb6323621c7b9acbd67466ca8cdea665b6597 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 22:53:10 +0900 Subject: [PATCH 11/24] =?UTF-8?q?refactor:=20=EB=8C=80=ED=9A=8C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=84=A0=ED=83=9D=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/CreateCompetitionPage.tsx | 57 +++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 857de15..2205f9a 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -60,15 +60,7 @@ export default function CompetitionCreatePage() { setEndsAt(newEndsAt); } - function handleSelectProblem(e: MouseEvent) { - const $target = e.target as HTMLElement; - if ($target.tagName !== 'BUTTON') return; - - const $li = $target.closest('li'); - if (!$li) return; - - const problemId = Number($li.dataset['problemId']); - + function handleSelectProblem(problemId: ProblemId) { if (pickedProblemIds.includes(problemId)) { setPickedProblemIds((ids) => ids.filter((id) => id !== problemId)); } else { @@ -139,16 +131,11 @@ export default function CompetitionCreatePage() { required > -
    - {allProblems.map(({ id, title }) => ( -
  • - - {id}: {title} - - {pickedProblemIds.includes(id) ? : } -
  • - ))} -
+
선택된 문제: {[...pickedProblemIds].sort().join(',')}
@@ -156,6 +143,38 @@ export default function CompetitionCreatePage() { ); } +interface ProblemListProps { + allProblems: ProblemInfo[]; + pickedProblemIds: ProblemId[]; + onSelectProblem: (problemId: ProblemId) => void; +} + +const ProblemList = ({ allProblems, pickedProblemIds, onSelectProblem }: ProblemListProps) => { + function handleSelectProblem(e: MouseEvent) { + const $target = e.target as HTMLElement; + if ($target.tagName !== 'BUTTON') return; + + const $li = $target.closest('li'); + if (!$li) return; + + const problemId = Number($li.dataset['problemId']); + onSelectProblem(problemId); + } + + return ( +
    + {allProblems.map(({ id, title }) => ( +
  • + + {id}: {title} + + +
  • + ))} +
+ ); +}; + const fieldSetStyle = css({ display: 'flex', flexDirection: 'column', From 3b319a1dfc538b5df5437bc41ebb0684e3d10c68 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 23:00:54 +0900 Subject: [PATCH 12/24] =?UTF-8?q?refactor:=20useProblemList=20=ED=9B=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/problem/useProblemList.ts | 33 ++++++++++++++++++++ frontend/src/pages/CreateCompetitionPage.tsx | 25 +++------------ 2 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 frontend/src/hooks/problem/useProblemList.ts diff --git a/frontend/src/hooks/problem/useProblemList.ts b/frontend/src/hooks/problem/useProblemList.ts new file mode 100644 index 0000000..e699387 --- /dev/null +++ b/frontend/src/hooks/problem/useProblemList.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; + +import type { ProblemId, ProblemInfo } from '@/apis/problems'; +import { fetchProblemList } from '@/apis/problems'; + +export function useProblemList() { + const [pickedProblemIds, setPickedProblemIds] = useState([]); + const [allProblems, setAllProblems] = useState([]); + + async function updateProblemList() { + const problems = await fetchProblemList(); + + setAllProblems(problems); + } + + function togglePickedProblem(problemId: ProblemId) { + if (pickedProblemIds.includes(problemId)) { + setPickedProblemIds((ids) => ids.filter((id) => id !== problemId).sort()); + } else { + setPickedProblemIds((ids) => [...ids, problemId].sort()); + } + } + + useEffect(() => { + updateProblemList(); + }, []); + + return { + allProblems, + pickedProblemIds, + togglePickedProblem, + }; +} diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 2205f9a..892fb29 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -1,13 +1,13 @@ import { css } from '@style/css'; import type { ChangeEvent, MouseEvent } from 'react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { CompetitionForm, createCompetition } from '@/apis/competitions'; import type { ProblemId, ProblemInfo } from '@/apis/problems'; -import { fetchProblemList } from '@/apis/problems'; import { Input } from '@/components/Common'; +import { useProblemList } from '@/hooks/problem/useProblemList'; import { formatDate, toLocalDate } from '@/utils/date'; import { isNil } from '@/utils/type'; @@ -22,18 +22,7 @@ export default function CompetitionCreatePage() { const [startsAt, setStartsAt] = useState(currentDateStr); const [endsAt, setEndsAt] = useState(currentDateStr); - const [pickedProblemIds, setPickedProblemIds] = useState([]); - const [allProblems, setAllProblems] = useState([]); - - async function updateProblemList() { - const problems = await fetchProblemList(); - - setAllProblems(problems); - } - - useEffect(() => { - updateProblemList(); - }, []); + const { pickedProblemIds, allProblems, togglePickedProblem } = useProblemList(); function handleChangeName(e: ChangeEvent) { const newName = e.target.value; @@ -61,11 +50,7 @@ export default function CompetitionCreatePage() { } function handleSelectProblem(problemId: ProblemId) { - if (pickedProblemIds.includes(problemId)) { - setPickedProblemIds((ids) => ids.filter((id) => id !== problemId)); - } else { - setPickedProblemIds((ids) => [...ids, problemId]); - } + togglePickedProblem(problemId); } async function handleSubmitCompetition() { @@ -136,7 +121,7 @@ export default function CompetitionCreatePage() { pickedProblemIds={pickedProblemIds} onSelectProblem={handleSelectProblem} > -
선택된 문제: {[...pickedProblemIds].sort().join(',')}
+
선택된 문제: {[...pickedProblemIds].join(',')}
From abf11be03bd1bff6f27b78b0152a5002315440cd Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 23:10:53 +0900 Subject: [PATCH 13/24] =?UTF-8?q?refactor:=20CompetitionForm=EC=9D=84=20?= =?UTF-8?q?=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/competition/useCompetitionForm.ts | 42 +++++++++++++++++++ frontend/src/pages/CreateCompetitionPage.tsx | 38 ++++++----------- 2 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 frontend/src/hooks/competition/useCompetitionForm.ts diff --git a/frontend/src/hooks/competition/useCompetitionForm.ts b/frontend/src/hooks/competition/useCompetitionForm.ts new file mode 100644 index 0000000..0331b7e --- /dev/null +++ b/frontend/src/hooks/competition/useCompetitionForm.ts @@ -0,0 +1,42 @@ +import { useState } from 'react'; + +import type { CompetitionForm } from '@/apis/competitions'; +import { formatDate, toLocalDate } from '@/utils/date'; + +export function useCompetitionForm(initialForm?: Partial) { + initialForm = initialForm ?? {}; + + const [name, setName] = useState(initialForm.name ?? ''); + const [detail, setDetail] = useState(initialForm.detail ?? ''); + const [maxParticipants, setMaxParticipants] = useState(initialForm.maxParticipants ?? 0); + + const currentDate = toLocalDate(new Date()); + const currentDateStr = formatDate(currentDate, 'YYYY-MM-DDThh:mm'); + + const [startsAt, setStartsAt] = useState(initialForm.startsAt ?? currentDateStr); + const [endsAt, setEndsAt] = useState(initialForm.endsAt ?? currentDateStr); + + function getAllFormData() { + return { + name, + detail, + maxParticipants, + startsAt: new Date(startsAt).toISOString(), + endsAt: new Date(endsAt).toISOString(), + }; + } + + return { + name, + detail, + maxParticipants, + startsAt, + endsAt, + setName, + setDetail, + setMaxParticipants, + setStartsAt, + setEndsAt, + getAllFormData, + }; +} diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 892fb29..67451f4 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -1,52 +1,44 @@ import { css } from '@style/css'; import type { ChangeEvent, MouseEvent } from 'react'; -import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { CompetitionForm, createCompetition } from '@/apis/competitions'; import type { ProblemId, ProblemInfo } from '@/apis/problems'; import { Input } from '@/components/Common'; +import { useCompetitionForm } from '@/hooks/competition/useCompetitionForm'; import { useProblemList } from '@/hooks/problem/useProblemList'; -import { formatDate, toLocalDate } from '@/utils/date'; import { isNil } from '@/utils/type'; export default function CompetitionCreatePage() { const navigate = useNavigate(); - const [name, setName] = useState(''); - const [detail, setDetail] = useState(''); - const [maxParticipants, setMaxParticipants] = useState(0); - const currentDate = toLocalDate(new Date()); - const currentDateStr = formatDate(currentDate, 'YYYY-MM-DDThh:mm'); - - const [startsAt, setStartsAt] = useState(currentDateStr); - const [endsAt, setEndsAt] = useState(currentDateStr); + const form = useCompetitionForm(); const { pickedProblemIds, allProblems, togglePickedProblem } = useProblemList(); function handleChangeName(e: ChangeEvent) { const newName = e.target.value; - setName(newName); + form.setName(newName); } function handleChangeDetail(e: ChangeEvent) { const newDetail = e.target.value; - setDetail(newDetail); + form.setDetail(newDetail); } function handleChangeMaxParticipants(e: ChangeEvent) { const newMaxParticipants = Number(e.target.value); - setMaxParticipants(newMaxParticipants); + form.setMaxParticipants(newMaxParticipants); } function handleChangeStartsAt(e: ChangeEvent) { const newStartsAt = e.target.value; - setStartsAt(newStartsAt); + form.setStartsAt(newStartsAt); } function handleChangeEndsAt(e: ChangeEvent) { const newEndsAt = e.target.value; - setEndsAt(newEndsAt); + form.setEndsAt(newEndsAt); } function handleSelectProblem(problemId: ProblemId) { @@ -55,11 +47,7 @@ export default function CompetitionCreatePage() { async function handleSubmitCompetition() { const competitionForm = { - name, - detail, - maxParticipants, - startsAt: new Date(startsAt).toISOString(), - endsAt: new Date(endsAt).toISOString(), + ...form.getAllFormData(), problems: pickedProblemIds, } satisfies CompetitionForm; @@ -76,7 +64,7 @@ export default function CompetitionCreatePage() { @@ -111,7 +99,7 @@ export default function CompetitionCreatePage() { From 14793f1313e311409156075c91062ac792a0df4b Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 23:15:09 +0900 Subject: [PATCH 14/24] =?UTF-8?q?refactor:=20pickedProblem=EC=9D=84=20Comp?= =?UTF-8?q?etitionForm=EC=9C=BC=EB=A1=9C=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/competition/useCompetitionForm.ts | 13 +++++++++++++ frontend/src/hooks/problem/useProblemList.ts | 13 +------------ frontend/src/pages/CreateCompetitionPage.tsx | 9 ++++----- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/frontend/src/hooks/competition/useCompetitionForm.ts b/frontend/src/hooks/competition/useCompetitionForm.ts index 0331b7e..7affa25 100644 --- a/frontend/src/hooks/competition/useCompetitionForm.ts +++ b/frontend/src/hooks/competition/useCompetitionForm.ts @@ -1,6 +1,7 @@ import { useState } from 'react'; import type { CompetitionForm } from '@/apis/competitions'; +import type { ProblemId } from '@/apis/problems'; import { formatDate, toLocalDate } from '@/utils/date'; export function useCompetitionForm(initialForm?: Partial) { @@ -15,6 +16,15 @@ export function useCompetitionForm(initialForm?: Partial) { const [startsAt, setStartsAt] = useState(initialForm.startsAt ?? currentDateStr); const [endsAt, setEndsAt] = useState(initialForm.endsAt ?? currentDateStr); + const [problems, setProblems] = useState([]); + + function togglePickedProblem(problemId: ProblemId) { + if (problems.includes(problemId)) { + setProblems((ids) => ids.filter((id) => id !== problemId).sort()); + } else { + setProblems((ids) => [...ids, problemId].sort()); + } + } function getAllFormData() { return { @@ -23,6 +33,7 @@ export function useCompetitionForm(initialForm?: Partial) { maxParticipants, startsAt: new Date(startsAt).toISOString(), endsAt: new Date(endsAt).toISOString(), + problems, }; } @@ -32,11 +43,13 @@ export function useCompetitionForm(initialForm?: Partial) { maxParticipants, startsAt, endsAt, + problems, setName, setDetail, setMaxParticipants, setStartsAt, setEndsAt, + togglePickedProblem, getAllFormData, }; } diff --git a/frontend/src/hooks/problem/useProblemList.ts b/frontend/src/hooks/problem/useProblemList.ts index e699387..012079e 100644 --- a/frontend/src/hooks/problem/useProblemList.ts +++ b/frontend/src/hooks/problem/useProblemList.ts @@ -1,10 +1,9 @@ import { useEffect, useState } from 'react'; -import type { ProblemId, ProblemInfo } from '@/apis/problems'; +import type { ProblemInfo } from '@/apis/problems'; import { fetchProblemList } from '@/apis/problems'; export function useProblemList() { - const [pickedProblemIds, setPickedProblemIds] = useState([]); const [allProblems, setAllProblems] = useState([]); async function updateProblemList() { @@ -13,21 +12,11 @@ export function useProblemList() { setAllProblems(problems); } - function togglePickedProblem(problemId: ProblemId) { - if (pickedProblemIds.includes(problemId)) { - setPickedProblemIds((ids) => ids.filter((id) => id !== problemId).sort()); - } else { - setPickedProblemIds((ids) => [...ids, problemId].sort()); - } - } - useEffect(() => { updateProblemList(); }, []); return { allProblems, - pickedProblemIds, - togglePickedProblem, }; } diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 67451f4..871c5bb 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -14,7 +14,7 @@ export default function CompetitionCreatePage() { const navigate = useNavigate(); const form = useCompetitionForm(); - const { pickedProblemIds, allProblems, togglePickedProblem } = useProblemList(); + const { allProblems } = useProblemList(); function handleChangeName(e: ChangeEvent) { const newName = e.target.value; @@ -42,13 +42,12 @@ export default function CompetitionCreatePage() { } function handleSelectProblem(problemId: ProblemId) { - togglePickedProblem(problemId); + form.togglePickedProblem(problemId); } async function handleSubmitCompetition() { const competitionForm = { ...form.getAllFormData(), - problems: pickedProblemIds, } satisfies CompetitionForm; const competitionInfo = await createCompetition(competitionForm); @@ -106,10 +105,10 @@ export default function CompetitionCreatePage() { -
선택된 문제: {[...pickedProblemIds].join(',')}
+
선택된 문제: {[...form.problems].join(',')}
From 18eb4af271cf96cdefc4da63987e684050e71d7f Mon Sep 17 00:00:00 2001 From: dev2820 Date: Wed, 22 Nov 2023 23:22:49 +0900 Subject: [PATCH 15/24] =?UTF-8?q?chore:=20gitignore=20=EB=A7=88=EC=A7=80?= =?UTF-8?q?=EB=A7=89=EC=97=90=20eol=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index 4c2885d..4e09b47 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -24,4 +24,4 @@ dist-ssr *.sw? # environment -.env \ No newline at end of file +.env From 8a14d99260f37ff9d4ba12824a1c2bfb167d0cd7 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 10:40:50 +0900 Subject: [PATCH 16/24] =?UTF-8?q?update:=20=EB=8C=80=ED=9A=8C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/competition/useCompetitionForm.ts | 67 ++++++++++++++++++- frontend/src/pages/CreateCompetitionPage.tsx | 32 +++++---- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/frontend/src/hooks/competition/useCompetitionForm.ts b/frontend/src/hooks/competition/useCompetitionForm.ts index 7affa25..92dde6d 100644 --- a/frontend/src/hooks/competition/useCompetitionForm.ts +++ b/frontend/src/hooks/competition/useCompetitionForm.ts @@ -1,15 +1,30 @@ import { useState } from 'react'; import type { CompetitionForm } from '@/apis/competitions'; +import { createCompetition } from '@/apis/competitions'; import type { ProblemId } from '@/apis/problems'; import { formatDate, toLocalDate } from '@/utils/date'; +type Valid = { + isValid: boolean; + message?: string; +}; + +const FIVE_MIN_BY_MS = 5 * 60 * 1000; +const VALIDATION_MESSAGE = { + needLongName: '이름은 1글자 이상이어야합니다', + needMoreParticipants: '최대 참여 인원은 1명 이상이어야 합니다', + tooEarlyStartTime: '대회 시작 시간은 현재보다 5분 늦은 시간부터 가능합니다', + tooEarlyEndTime: '대회 종료 시간은 대회 시작시간보다 늦게 끝나야합니다', + needMoreProblems: '대회 문제는 1개 이상이어야합니다', +}; + export function useCompetitionForm(initialForm?: Partial) { initialForm = initialForm ?? {}; const [name, setName] = useState(initialForm.name ?? ''); const [detail, setDetail] = useState(initialForm.detail ?? ''); - const [maxParticipants, setMaxParticipants] = useState(initialForm.maxParticipants ?? 0); + const [maxParticipants, setMaxParticipants] = useState(initialForm.maxParticipants ?? 1); const currentDate = toLocalDate(new Date()); const currentDateStr = formatDate(currentDate, 'YYYY-MM-DDThh:mm'); @@ -26,7 +41,7 @@ export function useCompetitionForm(initialForm?: Partial) { } } - function getAllFormData() { + function getAllFormData(): CompetitionForm { return { name, detail, @@ -37,6 +52,52 @@ export function useCompetitionForm(initialForm?: Partial) { }; } + async function submitCompetition(formData: CompetitionForm) { + return await createCompetition(formData); + } + + function validateForm(formData: CompetitionForm): Valid { + const { name, maxParticipants, startsAt, endsAt, problems } = formData; + if (name.length <= 0) { + return { + isValid: false, + message: VALIDATION_MESSAGE.needLongName, + }; + } + + if (maxParticipants <= 0) { + return { + isValid: false, + message: VALIDATION_MESSAGE.needMoreParticipants, + }; + } + + if (new Date(startsAt) > new Date(Date.now() + FIVE_MIN_BY_MS)) { + return { + isValid: false, + message: VALIDATION_MESSAGE.tooEarlyStartTime, + }; + } + + if (new Date(endsAt) > new Date(startsAt)) { + return { + isValid: false, + message: VALIDATION_MESSAGE.tooEarlyEndTime, + }; + } + + if (problems.length <= 0) { + return { + isValid: false, + message: VALIDATION_MESSAGE.needMoreProblems, + }; + } + + return { + isValid: true, + }; + } + return { name, detail, @@ -51,5 +112,7 @@ export function useCompetitionForm(initialForm?: Partial) { setEndsAt, togglePickedProblem, getAllFormData, + submitCompetition, + validateForm, }; } diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 871c5bb..5b3c77d 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -3,7 +3,6 @@ import { css } from '@style/css'; import type { ChangeEvent, MouseEvent } from 'react'; import { useNavigate } from 'react-router-dom'; -import { CompetitionForm, createCompetition } from '@/apis/competitions'; import type { ProblemId, ProblemInfo } from '@/apis/problems'; import { Input } from '@/components/Common'; import { useCompetitionForm } from '@/hooks/competition/useCompetitionForm'; @@ -45,16 +44,23 @@ export default function CompetitionCreatePage() { form.togglePickedProblem(problemId); } - async function handleSubmitCompetition() { - const competitionForm = { - ...form.getAllFormData(), - } satisfies CompetitionForm; - - const competitionInfo = await createCompetition(competitionForm); - if (isNil(competitionInfo)) return; - - const { id } = competitionInfo; - navigate(`/contest/detail/${id}`); + async function handleSumbitCompetition() { + const formData = form.getAllFormData(); + const { isValid, message } = form.validateForm(formData); + if (!isValid) { + if (!isNil(message)) { + alert(message); + } + return; + } + + const competition = await form.submitCompetition(formData); + if (isNil(competition)) { + alert('Oops... 대회 생성에 실패했습니다'); + return; + } + + navigate(`/contest/detail/${competition.id}`); } return ( @@ -82,7 +88,7 @@ export default function CompetitionCreatePage() { @@ -110,7 +116,7 @@ export default function CompetitionCreatePage() { >
선택된 문제: {[...form.problems].join(',')}
- + ); } From c06bf5de2ef9b3809980a79624de12c08b5f7bc4 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 10:43:10 +0900 Subject: [PATCH 17/24] =?UTF-8?q?update:=20competition/create=20->=20conte?= =?UTF-8?q?st/create=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index de0611f..78a06ed 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -20,7 +20,7 @@ const router = createBrowserRouter([ element: , }, { - path: '/competition/create', + path: '/contest/create', element: , }, ], From 9f3961b767883e4fa6dfe2a623de6399c29fc90e Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 10:49:14 +0900 Subject: [PATCH 18/24] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=A0=EB=8B=B9=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/competition/useCompetitionForm.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/hooks/competition/useCompetitionForm.ts b/frontend/src/hooks/competition/useCompetitionForm.ts index 92dde6d..f8f8a38 100644 --- a/frontend/src/hooks/competition/useCompetitionForm.ts +++ b/frontend/src/hooks/competition/useCompetitionForm.ts @@ -19,9 +19,7 @@ const VALIDATION_MESSAGE = { needMoreProblems: '대회 문제는 1개 이상이어야합니다', }; -export function useCompetitionForm(initialForm?: Partial) { - initialForm = initialForm ?? {}; - +export function useCompetitionForm(initialForm: Partial = {}) { const [name, setName] = useState(initialForm.name ?? ''); const [detail, setDetail] = useState(initialForm.detail ?? ''); const [maxParticipants, setMaxParticipants] = useState(initialForm.maxParticipants ?? 1); From e1c0a16675957d56ae25ac48e6e69dbbcb6a2c00 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 10:52:03 +0900 Subject: [PATCH 19/24] =?UTF-8?q?update:=20=EB=8C=80=ED=9A=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EA=B8=B0=20=EC=A0=9C=EB=AA=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/CreateCompetitionPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 5b3c77d..4a0eb5b 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -65,6 +65,7 @@ export default function CompetitionCreatePage() { return (
+

대회 생성 하기

Date: Thu, 23 Nov 2023 10:55:54 +0900 Subject: [PATCH 20/24] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EB=8C=80=ED=9A=8C=20=EC=83=9D=EC=84=B1=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시작 시작이 옳게 설정되면 에러가 나도록 하고 있었음 - 종료 시간도 마찬가지 --- frontend/src/hooks/competition/useCompetitionForm.ts | 5 ++--- frontend/src/pages/CreateCompetitionPage.tsx | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/competition/useCompetitionForm.ts b/frontend/src/hooks/competition/useCompetitionForm.ts index f8f8a38..8f2c181 100644 --- a/frontend/src/hooks/competition/useCompetitionForm.ts +++ b/frontend/src/hooks/competition/useCompetitionForm.ts @@ -69,15 +69,14 @@ export function useCompetitionForm(initialForm: Partial = {}) { message: VALIDATION_MESSAGE.needMoreParticipants, }; } - - if (new Date(startsAt) > new Date(Date.now() + FIVE_MIN_BY_MS)) { + if (new Date(startsAt) <= new Date(Date.now() + FIVE_MIN_BY_MS)) { return { isValid: false, message: VALIDATION_MESSAGE.tooEarlyStartTime, }; } - if (new Date(endsAt) > new Date(startsAt)) { + if (new Date(endsAt) <= new Date(startsAt)) { return { isValid: false, message: VALIDATION_MESSAGE.tooEarlyEndTime, diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 4a0eb5b..f0c011a 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -47,6 +47,7 @@ export default function CompetitionCreatePage() { async function handleSumbitCompetition() { const formData = form.getAllFormData(); const { isValid, message } = form.validateForm(formData); + if (!isValid) { if (!isNil(message)) { alert(message); From 804d8e900256ac773db9f8f1a061c27e369547d8 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 10:57:30 +0900 Subject: [PATCH 21/24] =?UTF-8?q?update:=20=EB=8C=80=ED=9A=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/CreateCompetitionPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index f0c011a..aaa217f 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -86,7 +86,7 @@ export default function CompetitionCreatePage() { required > - + - + Date: Thu, 23 Nov 2023 11:11:48 +0900 Subject: [PATCH 22/24] =?UTF-8?q?refactor:=20DetailPage=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EB=8A=94=20=EA=B2=BD=EB=A1=9C=20=EC=83=81=EC=88=98?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/problem/useProblemList.ts | 6 +++--- frontend/src/pages/CreateCompetitionPage.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/hooks/problem/useProblemList.ts b/frontend/src/hooks/problem/useProblemList.ts index 012079e..31de0f6 100644 --- a/frontend/src/hooks/problem/useProblemList.ts +++ b/frontend/src/hooks/problem/useProblemList.ts @@ -4,12 +4,12 @@ import type { ProblemInfo } from '@/apis/problems'; import { fetchProblemList } from '@/apis/problems'; export function useProblemList() { - const [allProblems, setAllProblems] = useState([]); + const [problemList, setProblemList] = useState([]); async function updateProblemList() { const problems = await fetchProblemList(); - setAllProblems(problems); + setProblemList(problems); } useEffect(() => { @@ -17,6 +17,6 @@ export function useProblemList() { }, []); return { - allProblems, + problemList, }; } diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index aaa217f..8f19d49 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -13,7 +13,7 @@ export default function CompetitionCreatePage() { const navigate = useNavigate(); const form = useCompetitionForm(); - const { allProblems } = useProblemList(); + const { problemList } = useProblemList(); function handleChangeName(e: ChangeEvent) { const newName = e.target.value; @@ -61,7 +61,8 @@ export default function CompetitionCreatePage() { return; } - navigate(`/contest/detail/${competition.id}`); + const TO_DETAIL_PAGE = `/contest/detail/${competition.id}`; + navigate(TO_DETAIL_PAGE); } return ( @@ -112,7 +113,7 @@ export default function CompetitionCreatePage() { > From da285b0e3add82a93c8bdf4e68b05ee174ee0ac0 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 11:16:58 +0900 Subject: [PATCH 23/24] =?UTF-8?q?refactor:=20selectableProblemList=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Problem/SelectableProblemList.tsx | 48 +++++++++++++++++++ frontend/src/pages/CreateCompetitionPage.tsx | 43 +++-------------- 2 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/Problem/SelectableProblemList.tsx diff --git a/frontend/src/components/Problem/SelectableProblemList.tsx b/frontend/src/components/Problem/SelectableProblemList.tsx new file mode 100644 index 0000000..db7bea5 --- /dev/null +++ b/frontend/src/components/Problem/SelectableProblemList.tsx @@ -0,0 +1,48 @@ +import type { MouseEvent } from 'react'; + +import type { ProblemId, ProblemInfo } from '@/apis/problems'; + +interface ProblemListProps { + problemList: ProblemInfo[]; + pickedProblemIds: ProblemId[]; + onSelectProblem: (problemId: ProblemId) => void; +} + +const SelectableProblemList = ({ + problemList, + pickedProblemIds, + onSelectProblem, +}: ProblemListProps) => { + function handleSelectProblem(e: MouseEvent) { + const $target = e.target as HTMLElement; + if ($target.tagName !== 'BUTTON') return; + + const $li = $target.closest('li'); + if (!$li) return; + + const problemId = Number($li.dataset['problemId']); + onSelectProblem(problemId); + } + + const getSelectButton = ({ isPicked }: { isPicked: boolean }) => { + if (isPicked) { + return ; + } + return ; + }; + + return ( +
    + {problemList.map(({ id, title }) => ( +
  • + + {id}: {title} + + {getSelectButton({ isPicked: pickedProblemIds.includes(id) })} +
  • + ))} +
+ ); +}; + +export default SelectableProblemList; diff --git a/frontend/src/pages/CreateCompetitionPage.tsx b/frontend/src/pages/CreateCompetitionPage.tsx index 8f19d49..3de0724 100644 --- a/frontend/src/pages/CreateCompetitionPage.tsx +++ b/frontend/src/pages/CreateCompetitionPage.tsx @@ -1,10 +1,11 @@ import { css } from '@style/css'; -import type { ChangeEvent, MouseEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { ProblemId, ProblemInfo } from '@/apis/problems'; +import type { ProblemId } from '@/apis/problems'; import { Input } from '@/components/Common'; +import SelectableProblemList from '@/components/Problem/SelectableProblemList'; import { useCompetitionForm } from '@/hooks/competition/useCompetitionForm'; import { useProblemList } from '@/hooks/problem/useProblemList'; import { isNil } from '@/utils/type'; @@ -112,11 +113,11 @@ export default function CompetitionCreatePage() { required > - + >
선택된 문제: {[...form.problems].join(',')}
@@ -124,38 +125,6 @@ export default function CompetitionCreatePage() { ); } -interface ProblemListProps { - allProblems: ProblemInfo[]; - pickedProblemIds: ProblemId[]; - onSelectProblem: (problemId: ProblemId) => void; -} - -const ProblemList = ({ allProblems, pickedProblemIds, onSelectProblem }: ProblemListProps) => { - function handleSelectProblem(e: MouseEvent) { - const $target = e.target as HTMLElement; - if ($target.tagName !== 'BUTTON') return; - - const $li = $target.closest('li'); - if (!$li) return; - - const problemId = Number($li.dataset['problemId']); - onSelectProblem(problemId); - } - - return ( -
    - {allProblems.map(({ id, title }) => ( -
  • - - {id}: {title} - - -
  • - ))} -
- ); -}; - const fieldSetStyle = css({ display: 'flex', flexDirection: 'column', From 8e0e6286696907ba5606978d8324d1f64096cb29 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Thu, 23 Nov 2023 11:20:01 +0900 Subject: [PATCH 24/24] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=A8=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Problem/SelectableProblemList.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Problem/SelectableProblemList.tsx b/frontend/src/components/Problem/SelectableProblemList.tsx index db7bea5..a5087cb 100644 --- a/frontend/src/components/Problem/SelectableProblemList.tsx +++ b/frontend/src/components/Problem/SelectableProblemList.tsx @@ -24,13 +24,6 @@ const SelectableProblemList = ({ onSelectProblem(problemId); } - const getSelectButton = ({ isPicked }: { isPicked: boolean }) => { - if (isPicked) { - return ; - } - return ; - }; - return (
    {problemList.map(({ id, title }) => ( @@ -38,7 +31,7 @@ const SelectableProblemList = ({ {id}: {title} - {getSelectButton({ isPicked: pickedProblemIds.includes(id) })} + ))}
@@ -46,3 +39,10 @@ const SelectableProblemList = ({ }; export default SelectableProblemList; + +const SelectButton = ({ isPicked }: { isPicked: boolean }) => { + if (isPicked) { + return ; + } + return ; +};