Skip to content

Commit

Permalink
Merge pull request #62 from sos-sejong-opensource-software/feat-class…
Browse files Browse the repository at this point in the history
…-problem

feat: contest problem 생성 / 편집 / 삭제 / 기존 problem 편집
  • Loading branch information
newminkyung authored Feb 10, 2023
2 parents 56f4b48 + a74f689 commit 2b86ba6
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 78 deletions.
8 changes: 6 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
ProblemData,
ProblemLeaderBoard,
ProblemSubmission,
ProblemForm,
ProblemCreate,
ProblemEdit,
ClassEditContestList,
AdminFaqList,
AdminNewFaq,
Expand Down Expand Up @@ -59,7 +60,10 @@ export default function App() {
<Route path={SUB_PATH.LEADERBOARD} element={<ProblemLeaderBoard />} />
<Route path={SUB_PATH.SUBMISSON} element={<ProblemSubmission />} />
</Route>
<Route path={SUB_PATH.PROBLEM_CREATE} element={<ProblemForm />}></Route>
<Route path={SUB_PATH.PROBLEM_CREATE} element={<ProblemCreate />} />
<Route path={SUB_PATH.PROBLEM_EDIT_LIST} element={<ClassProblemList />} />
<Route path={SUB_PATH.PROBLEM_EDIT} element={<ProblemEdit />} />
<Route path={SUB_PATH.CONTEST_PROBLEM_EDIT} element={<ProblemEdit />} />
</Route>
<Route path={SUB_PATH.PROBLEM} element={<AllProblemDetail />}>
<Route path={SUB_PATH.DESCRIPTION} element={<ProblemDescription />} />
Expand Down
1 change: 1 addition & 0 deletions src/components/atom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './ErrorMessage';
export * from './Select';
export * from './LinkButton';
export * from './Switch';
export * from './Editor';
5 changes: 4 additions & 1 deletion src/constants/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
};

Expand Down
10 changes: 7 additions & 3 deletions src/pages/Class/ClassContestProblemList.tsx
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -14,7 +17,7 @@ export function ClassContestProblemList() {
const tableProps = useContestProblemListTable(classId, contestId);

const handleCreateButtonClick = () => {
navigate('create');
setShowModal(true);
};

return (
Expand All @@ -24,6 +27,7 @@ export function ClassContestProblemList() {
<Button onClick={handleCreateButtonClick}>문제 생성</Button>
</header>
<Table {...tableProps} />
<ContestProblemCreateModal showModal={showModal} setShowModal={setShowModal} />
</>
);
}
7 changes: 7 additions & 0 deletions src/pages/Class/Problem/ProblemCreate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ProblemForm } from './components';
import { useCreateProblemMutation } from './hooks';

export function ProblemCreate() {
const { mutate } = useCreateProblemMutation();
return <ProblemForm mutate={mutate} />;
}
12 changes: 12 additions & 0 deletions src/pages/Class/Problem/ProblemEdit.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProblemForm data={{ ...data, data: new Blob(), solution: new Blob() }} mutate={mutate} />;
}
62 changes: 0 additions & 62 deletions src/pages/Class/Problem/ProblemForm.tsx

This file was deleted.

5 changes: 5 additions & 0 deletions src/pages/Class/Problem/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
};
Expand Down Expand Up @@ -46,6 +50,7 @@ const createContestProblemSumbissionCheck = ({
export {
getProblem,
createProblem,
editProblem,
getContestProblem,
getContestProblemSubmission,
createContestProblemSubmission,
Expand Down
74 changes: 74 additions & 0 deletions src/pages/Class/Problem/components/ProblemForm.tsx
Original file line number Diff line number Diff line change
@@ -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<T extends React.ElementType> = Component<T> & {
data?: ProblemRequest;
mutate: UseMutateFunction<AxiosResponse<any, any>, AxiosError<unknown, any>, 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<HTMLFormElement>) => {
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 (
<form onSubmit={handleFormSubmit} className="flex flex-col gap-2">
<Heading as="h3">{data ? '문제 편집' : '문제 생성'}</Heading>
<Input required placeholder="문제 이름을 입력하세요." name="title" defaultValue={title} />
<div>
<Heading as="h4">문제 설명</Heading>
<Editor value={description} onChange={(value) => setDescription(value)} />
<Label>평가 지표</Label>
<Select required options={options} name="evaluation" defaultValue={evaluation} />
<Label>전체 공개</Label>
<Switch enabled={_public ?? true} name="public" />
</div>
<div>
<Heading as="h4">데이터</Heading>
<Editor value={dataDescription} onChange={(value) => setDataDescription(value)} />
<Label>데이터 파일</Label>
<Input type="file" accept=".zip" required name="data" />
<Label>정답 파일</Label>
<Input type="file" accept=".csv" required name="solution" />
</div>
<Button>저장</Button>
</form>
);
}
1 change: 1 addition & 0 deletions src/pages/Class/Problem/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Problem';
export * from './FileSubmissionForm';
export * from './LeaderboardSubmissionForm';
export * from './ProblemForm';
1 change: 1 addition & 0 deletions src/pages/Class/Problem/hooks/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './useProblemQuery';
export * from './useCreateProblemMutation';
export * from './useEditProblemMutation';
export * from './useContestProblemQuery';
export * from './useContestProblemSubmissionQuery';
export * from './useCreateContestProblemSubmissionMutation';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AxiosResponse, AxiosError, FormData>
) => {
const navigate = useNavigate();
return useMutation((payload) => createProblem(payload), {
...options,
onSuccess: () => {
navigate(-1);
},
});
};
16 changes: 16 additions & 0 deletions src/pages/Class/Problem/hooks/query/useEditProblemMutation.ts
Original file line number Diff line number Diff line change
@@ -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<AxiosResponse, AxiosError, FormData>
) => {
return useMutation((payload) => editProblem(problemId, payload), {
...options,
onSuccess: () => {
alert('편집 완료');
},
});
};
3 changes: 2 additions & 1 deletion src/pages/Class/Problem/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './ProblemForm';
export * from './ProblemCreate';
export * from './ProblemEdit';

export * from './AllProblemDetail';
export * from './ContestProblemDetail';
Expand Down
37 changes: 37 additions & 0 deletions src/pages/Class/components/ContestProblemCreateModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useNavigate } from 'react-router-dom';

import { Modal, Button } from '@/components';
import { StateAndAction } from '@/types/state';

type ContestProblemCreateModal<T extends React.ElementType> = Component<T> &
StateAndAction<boolean, 'showModal'>;

export function ContestProblemCreateModal({
showModal,
setShowModal,
}: ContestProblemCreateModal<'div'>) {
const navigate = useNavigate();

const handleCreateButtonClick = () => {
navigate('create');
};

const handleGetButtonClick = () => {
navigate('edit');
};

return (
<Modal open={showModal}>
<Modal.Header>문제 만들기</Modal.Header>
<Modal.Body className="w-[300px] flex flex-col gap-2">
<Button onClick={handleCreateButtonClick}>새로운 문제 만들기</Button>
<Button onClick={handleGetButtonClick}>기존 문제 가져오기</Button>
</Modal.Body>
<Modal.Footer className="gap-2">
<Button color="error" type="button" onClick={() => setShowModal(false)}>
닫기
</Button>
</Modal.Footer>
</Modal>
);
}
1 change: 1 addition & 0 deletions src/pages/Class/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './ClassStudentForm';
export * from './ClassFormModal';
export * from './ContestFormModal';
export * from './ContestEditModal';
export * from './ContestProblemCreateModal';
2 changes: 1 addition & 1 deletion src/pages/Class/hooks/useClassProblemListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const useClassProblemListTable = (keyword: string) => {
e: React.MouseEvent<HTMLTableRowElement, MouseEvent>,
id: number | string
) => {
navigate(`../${id}`);
navigate(`${id}`);
};

return {
Expand Down
Loading

0 comments on commit 2b86ba6

Please sign in to comment.