diff --git a/src/App.tsx b/src/App.tsx index bc8254c..bca4baf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,7 +25,8 @@ import { ProblemData, ProblemLeaderBoard, ProblemSubmission, - ProblemForm, + ProblemCreate, + ProblemEdit, ClassEditContestList, AdminFaqList, AdminNewFaq, @@ -59,7 +60,10 @@ export default function App() { } /> } /> - }> + } /> + } /> + } /> + } /> }> } /> diff --git a/src/components/atom/index.ts b/src/components/atom/index.ts index 2462131..224856a 100644 --- a/src/components/atom/index.ts +++ b/src/components/atom/index.ts @@ -6,3 +6,4 @@ export * from './ErrorMessage'; export * from './Select'; export * from './LinkButton'; export * from './Switch'; +export * from './Editor'; diff --git a/src/constants/paths.ts b/src/constants/paths.ts index 50d6af7..47779a9 100644 --- a/src/constants/paths.ts +++ b/src/constants/paths.ts @@ -21,8 +21,10 @@ export const SUB_PATH = { LEADERBOARD: 'leaderboard', SUBMISSON: 'submission', - PROBLEM: ':problemId', + PROBLEM: 'all-problems/:problemId', PROBLEM_CREATE: ':contestId/create', + PROBLEM_EDIT_LIST: ':contestId/edit', + PROBLEM_EDIT: ':contestId/edit/:problemId', ALL_PROBLEMS: 'all-problems', ALL_CLASSES: 'all-classes', ANNOUNCEMENTS: 'announcements', @@ -33,6 +35,7 @@ export const SUB_PATH = { CONTEST: 'contest', CONTEST_DETAIL: ':contestId', CONTEST_PROBLEM: ':contestId/:contestProblemId', + CONTEST_PROBLEM_EDIT: ':contestId/:problemId/edit', CONTEST_LIST_EDIT: 'edit', }; diff --git a/src/pages/Class/ClassContestProblemList.tsx b/src/pages/Class/ClassContestProblemList.tsx index 69573c9..88c0c94 100644 --- a/src/pages/Class/ClassContestProblemList.tsx +++ b/src/pages/Class/ClassContestProblemList.tsx @@ -1,10 +1,13 @@ -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; +import { useState } from 'react'; import { Button, Heading, Table } from '@/components'; + import { useContestProblemListTable, useClassContestListQuery } from './hooks'; +import { ContestProblemCreateModal } from './components'; export function ClassContestProblemList() { - const navigate = useNavigate(); + const [showModal, setShowModal] = useState(false); const { classId, contestId } = useParams() as { classId: string; contestId: string }; const { data: { results }, @@ -14,7 +17,7 @@ export function ClassContestProblemList() { const tableProps = useContestProblemListTable(classId, contestId); const handleCreateButtonClick = () => { - navigate('create'); + setShowModal(true); }; return ( @@ -24,6 +27,7 @@ export function ClassContestProblemList() { + ); } diff --git a/src/pages/Class/Problem/ProblemCreate.tsx b/src/pages/Class/Problem/ProblemCreate.tsx new file mode 100644 index 0000000..11f7a50 --- /dev/null +++ b/src/pages/Class/Problem/ProblemCreate.tsx @@ -0,0 +1,7 @@ +import { ProblemForm } from './components'; +import { useCreateProblemMutation } from './hooks'; + +export function ProblemCreate() { + const { mutate } = useCreateProblemMutation(); + return ; +} diff --git a/src/pages/Class/Problem/ProblemEdit.tsx b/src/pages/Class/Problem/ProblemEdit.tsx new file mode 100644 index 0000000..cb60100 --- /dev/null +++ b/src/pages/Class/Problem/ProblemEdit.tsx @@ -0,0 +1,12 @@ +import { useParams } from 'react-router-dom'; +import { ProblemForm } from './components'; +import { useEditProblemMutation, useProblemQuery } from './hooks'; + +export function ProblemEdit() { + const { problemId } = useParams() as { problemId: string }; + /** FIXME: 파일이 존재하는 쿼리로 변경 */ + const { data } = useProblemQuery(problemId); + const { mutate } = useEditProblemMutation(problemId); + + return ; +} diff --git a/src/pages/Class/Problem/ProblemForm.tsx b/src/pages/Class/Problem/ProblemForm.tsx deleted file mode 100644 index 62e9ced..0000000 --- a/src/pages/Class/Problem/ProblemForm.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useParams } from 'react-router-dom'; - -import { Button, Heading, Label, Input, Select, Switch } from '@/components'; -import { METRICS } from '@/constants'; - -import { useCreateProblemMutation } from './hooks'; - -export function ProblemForm() { - const options = METRICS.map((metric) => ({ value: metric })); - const { id: classId } = useParams() as { id: string }; - const { mutate: createProblem } = useCreateProblemMutation(); - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const formValue = Object.values(e.target) - .filter(({ nodeName }) => nodeName === 'INPUT' || nodeName === 'SELECT') - .map(({ value, files }) => (files ? files[0] : value)); - - const payloadKey = [ - 'title', - 'description', - 'evaluation', - 'public', - 'data_description', - 'data', - 'solution', - ]; - - const formData = new FormData(); - formData.append('class_id', classId); - payloadKey.forEach((key, index) => formData.append(key, formValue[index])); - - createProblem(formData); - }; - - return ( -
- 문제 생성 - -
- 문제 설명 - {/* FIXME: 마크다운 */} - - - - - - - -
- - - ); -} diff --git a/src/pages/Class/Problem/api/index.ts b/src/pages/Class/Problem/api/index.ts index 391a791..da098f9 100644 --- a/src/pages/Class/Problem/api/index.ts +++ b/src/pages/Class/Problem/api/index.ts @@ -10,6 +10,10 @@ const createProblem = (payload: FormData) => { return fileApi.post(`${API_URL}/`, payload); }; +const editProblem = (problemId: string, payload: FormData) => { + return fileApi.put(`${API_URL}/${problemId}/`, payload); +}; + const getContestProblem = ({ classId, contestId, contestProblemId }: ContestProblemRequest) => { return api.get(`/class/${classId}/contests/${contestId}/${contestProblemId}`); }; @@ -46,6 +50,7 @@ const createContestProblemSumbissionCheck = ({ export { getProblem, createProblem, + editProblem, getContestProblem, getContestProblemSubmission, createContestProblemSubmission, diff --git a/src/pages/Class/Problem/components/ProblemForm.tsx b/src/pages/Class/Problem/components/ProblemForm.tsx new file mode 100644 index 0000000..bfe038f --- /dev/null +++ b/src/pages/Class/Problem/components/ProblemForm.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { UseMutateFunction } from 'react-query'; +import { AxiosResponse, AxiosError } from 'axios'; + +import { Button, Heading, Label, Input, Select, Switch, Editor } from '@/components'; +import { METRICS } from '@/constants'; + +type ProblemFormProps = Component & { + data?: ProblemRequest; + mutate: UseMutateFunction, AxiosError, FormData, unknown>; +}; + +export function ProblemForm({ data, mutate }: ProblemFormProps<'form'>) { + const { + title, + description: _description, + evaluation, + public: _public, + data_description, + data: dataFile, + solution, + } = data ?? {}; + const [description, setDescription] = useState(_description ?? ''); + const [dataDescription, setDataDescription] = useState(data_description ?? ''); + + const { classId } = useParams() as { classId: string }; + + const options = METRICS.map((metric) => ({ value: metric })); + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const payloadKey = ['title', 'evaluation', 'public', 'data', 'solution']; + + const formValue = Object.values(e.target) + .filter(({ name }) => payloadKey.includes(name)) + .map(({ name, checked, value, files }) => + name === 'public' ? checked : files ? files[0] : value + ); + + const formData = new FormData(); + formData.append('class_id', classId); + formData.append('description', description); + formData.append('data_description', dataDescription); + payloadKey.forEach((key, index) => formData.append(key, formValue[index])); + + mutate(formData); + }; + + return ( +
+ {data ? '문제 편집' : '문제 생성'} + +
+ 문제 설명 + setDescription(value)} /> + + + + +
+ + + ); +} diff --git a/src/pages/Class/Problem/components/index.ts b/src/pages/Class/Problem/components/index.ts index ef243d0..99e64d9 100644 --- a/src/pages/Class/Problem/components/index.ts +++ b/src/pages/Class/Problem/components/index.ts @@ -1,3 +1,4 @@ export * from './Problem'; export * from './FileSubmissionForm'; export * from './LeaderboardSubmissionForm'; +export * from './ProblemForm'; diff --git a/src/pages/Class/Problem/hooks/query/index.ts b/src/pages/Class/Problem/hooks/query/index.ts index b25562f..d894dac 100644 --- a/src/pages/Class/Problem/hooks/query/index.ts +++ b/src/pages/Class/Problem/hooks/query/index.ts @@ -1,5 +1,6 @@ export * from './useProblemQuery'; export * from './useCreateProblemMutation'; +export * from './useEditProblemMutation'; export * from './useContestProblemQuery'; export * from './useContestProblemSubmissionQuery'; export * from './useCreateContestProblemSubmissionMutation'; diff --git a/src/pages/Class/Problem/hooks/query/useCreateProblemMutation.ts b/src/pages/Class/Problem/hooks/query/useCreateProblemMutation.ts index 2e8bec1..e1070a3 100644 --- a/src/pages/Class/Problem/hooks/query/useCreateProblemMutation.ts +++ b/src/pages/Class/Problem/hooks/query/useCreateProblemMutation.ts @@ -1,12 +1,17 @@ import { useMutation, UseMutationOptions } from 'react-query'; import { AxiosError, AxiosResponse } from 'axios'; +import { useNavigate } from 'react-router-dom'; import { createProblem } from '../../api'; export const useCreateProblemMutation = ( options?: UseMutationOptions ) => { + const navigate = useNavigate(); return useMutation((payload) => createProblem(payload), { ...options, + onSuccess: () => { + navigate(-1); + }, }); }; diff --git a/src/pages/Class/Problem/hooks/query/useEditProblemMutation.ts b/src/pages/Class/Problem/hooks/query/useEditProblemMutation.ts new file mode 100644 index 0000000..98adc1a --- /dev/null +++ b/src/pages/Class/Problem/hooks/query/useEditProblemMutation.ts @@ -0,0 +1,16 @@ +import { useMutation, UseMutationOptions } from 'react-query'; +import { AxiosError, AxiosResponse } from 'axios'; + +import { editProblem } from '../../api'; + +export const useEditProblemMutation = ( + problemId: string, + options?: UseMutationOptions +) => { + return useMutation((payload) => editProblem(problemId, payload), { + ...options, + onSuccess: () => { + alert('편집 완료'); + }, + }); +}; diff --git a/src/pages/Class/Problem/index.ts b/src/pages/Class/Problem/index.ts index b999143..1806f2f 100644 --- a/src/pages/Class/Problem/index.ts +++ b/src/pages/Class/Problem/index.ts @@ -1,4 +1,5 @@ -export * from './ProblemForm'; +export * from './ProblemCreate'; +export * from './ProblemEdit'; export * from './AllProblemDetail'; export * from './ContestProblemDetail'; diff --git a/src/pages/Class/components/ContestProblemCreateModal.tsx b/src/pages/Class/components/ContestProblemCreateModal.tsx new file mode 100644 index 0000000..4d8ae16 --- /dev/null +++ b/src/pages/Class/components/ContestProblemCreateModal.tsx @@ -0,0 +1,37 @@ +import { useNavigate } from 'react-router-dom'; + +import { Modal, Button } from '@/components'; +import { StateAndAction } from '@/types/state'; + +type ContestProblemCreateModal = Component & + StateAndAction; + +export function ContestProblemCreateModal({ + showModal, + setShowModal, +}: ContestProblemCreateModal<'div'>) { + const navigate = useNavigate(); + + const handleCreateButtonClick = () => { + navigate('create'); + }; + + const handleGetButtonClick = () => { + navigate('edit'); + }; + + return ( + + 문제 만들기 + + + + + + + + + ); +} diff --git a/src/pages/Class/components/index.ts b/src/pages/Class/components/index.ts index ca98a09..15a2e2b 100644 --- a/src/pages/Class/components/index.ts +++ b/src/pages/Class/components/index.ts @@ -3,3 +3,4 @@ export * from './ClassStudentForm'; export * from './ClassFormModal'; export * from './ContestFormModal'; export * from './ContestEditModal'; +export * from './ContestProblemCreateModal'; diff --git a/src/pages/Class/hooks/useClassProblemListTable.tsx b/src/pages/Class/hooks/useClassProblemListTable.tsx index db2ae36..0506f5b 100644 --- a/src/pages/Class/hooks/useClassProblemListTable.tsx +++ b/src/pages/Class/hooks/useClassProblemListTable.tsx @@ -57,7 +57,7 @@ export const useClassProblemListTable = (keyword: string) => { e: React.MouseEvent, id: number | string ) => { - navigate(`../${id}`); + navigate(`${id}`); }; return { diff --git a/src/pages/Class/hooks/useContestProblemListTable.tsx b/src/pages/Class/hooks/useContestProblemListTable.tsx index 7e2b0f9..edb5b14 100644 --- a/src/pages/Class/hooks/useContestProblemListTable.tsx +++ b/src/pages/Class/hooks/useContestProblemListTable.tsx @@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom'; import { Button } from '@/components'; -import { useClassContestProblemListQuery } from './query'; +import { useClassContestProblemListQuery, useDeleteProblemMutation } from './query'; import { formatTime } from '@/utils/time'; export const useContestProblemListTable = (classId: string, contestId: string) => { @@ -16,21 +16,39 @@ export const useContestProblemListTable = (classId: string, contestId: string) = { Header: '삭제', accessor: 'delete' }, ]; + const handleEditButtonClick = (e: React.MouseEvent, id: number) => { + e.stopPropagation(); + navigate(`${id}/edit`); + }; + + const { mutate: deleteProblem } = useDeleteProblemMutation(); + const handleDeleteButtonClick = ( + e: React.MouseEvent, + id: number, + title: string + ) => { + e.stopPropagation(); + const isConfirmed = confirm(`${title}을 삭제하시겠습니까?`); + if (isConfirmed) deleteProblem(`${id}`); + }; + const { data: { results }, } = useClassContestProblemListQuery(classId, contestId); - const data = results.map((problem) => ({ - ...problem, - endTime: formatTime(problem.end_time), - edit: , - delete: , - })); + const data = results.map((problem) => { + const { problem_id, title, end_time } = problem; + return { + ...problem, + endTime: formatTime(end_time), + edit: , + delete: , + }; + }); const handleRowClick = ( e: React.MouseEvent, id: number | string ) => { - /** FIXME: 해당 문제 상세 페이지로 이동하게 수정 */ navigate(`${id}`); };