Skip to content

Commit

Permalink
Merge pull request #58 from sos-sejong-opensource-software/problem
Browse files Browse the repository at this point in the history
feat: 수업 문제 상세 조회 - 리더보드 / 제출
  • Loading branch information
newminkyung authored Feb 10, 2023
2 parents 5e5ed38 + efcab79 commit 5870054
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 38 deletions.
9 changes: 8 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
AdminNewAnnouncement,
AdminEditAnnouncement,
AllProblemDetail,
ContestProblemDetail,
ProblemDescription,
ProblemData,
ProblemLeaderBoard,
Expand Down Expand Up @@ -51,14 +52,20 @@ export default function App() {
<Route path={SUB_PATH.CONTEST} element={<ClassContest />}>
<Route path={SUB_PATH.CONTEST_LIST_EDIT} element={<ClassEditContestList />} />
<Route path={SUB_PATH.CONTEST_DETAIL} element={<ClassContestProblemList />} />
<Route path={SUB_PATH.CONTEST_PROBLEM} element={<ContestProblemDetail />}>
<Route path={SUB_PATH.DESCRIPTION} element={<ProblemDescription />} />
<Route path={SUB_PATH.DATA} element={<ProblemData />} />
<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>
<Route path={SUB_PATH.PROBLEM} element={<AllProblemDetail />}>
<Route path={SUB_PATH.DESCRIPTION} element={<ProblemDescription />} />
<Route path={SUB_PATH.DATA} element={<ProblemData />} />
<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>
<Route path={PATH.CLASS_LIST} element={<ClassList />}></Route>
<Route path={PATH.BOARD_LIST} element={<div>BoardList</div>} />
Expand Down
3 changes: 2 additions & 1 deletion src/constants/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const SUB_PATH = {
SUBMISSON: 'submission',

PROBLEM: ':problemId',
PROBLEM_CREATE: 'create',
PROBLEM_CREATE: ':contestId/create',
ALL_PROBLEMS: 'all-problems',
ALL_CLASSES: 'all-classes',
ANNOUNCEMENTS: 'announcements',
Expand All @@ -32,6 +32,7 @@ export const SUB_PATH = {
STUDENT_MANAGEMENT: 'student-management',
CONTEST: 'contest',
CONTEST_DETAIL: ':contestId',
CONTEST_PROBLEM: ':contestId/:contestProblemId',
CONTEST_LIST_EDIT: 'edit',
};

Expand Down
2 changes: 2 additions & 0 deletions src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ export const QUERY_KEYS = {
CLASS_STUDENT: 'class-student',
CLASS_TA: 'class-ta',
CLASS_CONTEST: 'class-contest',
CLASS_CONTEST_PROBLEM: 'class-contest-problem',
CLASS_CONTEST_PROBLEM_SUBMISSION: 'class-contest-problem-submission',
};
9 changes: 7 additions & 2 deletions src/pages/Class/ClassContestProblemList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';

import { Button, Heading, Table } from '@/components';
import { useContestProblemListTable, useClassContestListQuery } from './hooks';

export function ClassContestProblemList() {
const navigate = useNavigate();
const { classId, contestId } = useParams() as { classId: string; contestId: string };
const {
data: { results },
Expand All @@ -12,11 +13,15 @@ export function ClassContestProblemList() {

const tableProps = useContestProblemListTable(classId, contestId);

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

return (
<>
<header className="flex justify-between items-center">
<Heading as="h3">{title}</Heading>
<Button>문제 생성</Button>
<Button onClick={handleCreateButtonClick}>문제 생성</Button>
</header>
<Table {...tableProps} />
</>
Expand Down
25 changes: 25 additions & 0 deletions src/pages/Class/Problem/ContestProblemDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useParams } from 'react-router-dom';

import { SUB_PATH } from '@/constants';

import { Problem } from './components';
import { useContestProblemQuery } from './hooks';

export function ContestProblemDetail() {
const { classId, contestId, contestProblemId } = useParams() as {
classId: string;
contestId: string;
contestProblemId: string;
};

const { data } = useContestProblemQuery({ classId, contestId, contestProblemId });

const menuList = [
{ name: '문제 설명', to: SUB_PATH.DESCRIPTION },
{ name: '데이터', to: SUB_PATH.DATA },
{ name: '리더보드', to: SUB_PATH.LEADERBOARD },
{ name: '제출', to: SUB_PATH.SUBMISSON },
];

return <Problem menuList={menuList} data={data} />;
}
28 changes: 22 additions & 6 deletions src/pages/Class/Problem/ProblemLeaderBoard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { useParams } from 'react-router-dom';

import { Table } from '@/components';
import { formatTime } from '@/utils/time';

import { useContestProblemSubmissionQuery } from './hooks';

export function ProblemLeaderBoard() {
const { classId, contestId, contestProblemId } = useParams() as {
classId: string;
contestId: string;
contestProblemId: string;
};
/** TODO: 파일 다운로드 부분 추가 */
const column = [
{ Header: '#', accessor: 'id' },
{ Header: '이름', accessor: 'name' },
{ Header: '이름', accessor: 'username' },
{ Header: '점수', accessor: 'score' },
{ Header: '제출 날짜', accessor: 'time' },
{ Header: '코드(.ipynb)', accessor: 'ipynbFile' },
{ Header: '답안(.csv)', accessor: 'csvFile' },
{ Header: '제출 날짜', accessor: 'submissionDate' },
];

/** FIXME: 수업 상세 정보 */
const data = [{ id: 1 }];
/** FIXME: 새로운 api로 교체해야함 */
const {
data: { results },
} = useContestProblemSubmissionQuery({ classId, contestId, contestProblemId });

const data = results
.sort(({ score: prev }, { score: next }) => next - prev)
.sort(({ created_time: prev }, { created_time: next }) => +new Date(prev) - +new Date(next))
.map((submission) => ({ ...submission, submissionDate: formatTime(submission.created_time) }));

return <Table column={column} data={data} />;
}
61 changes: 36 additions & 25 deletions src/pages/Class/Problem/ProblemSubmission.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
import { Button, Heading, Table, Input } from '@/components';
import { useParams } from 'react-router-dom';

import {
useCreateContestProblemSubmissionCheckMutation,
useCreateContestProblemSubmissionMutation,
} from './hooks';

import { FileSubmissionForm, LeaderboardSubmissionForm } from './components';

export function ProblemSubmission() {
const column = [
{ Header: '#', accessor: 'id' },
{ Header: 'csv 파일', accessor: 'csvFile' },
{ Header: 'ipynb 파일', accessor: 'ipynbFile' },
{ Header: '점수', accessor: 'score' },
{ Header: 'status', accessor: 'status' },
{ Header: '제출 날짜', accessor: 'submissionDate' },
];
/** FIXME: 수업 상세 정보 */
const data = [{ id: 1 }];
const { classId, contestId, contestProblemId } = useParams() as {
classId: string;
contestId: string;
contestProblemId: string;
};

return (
<>
<form className="flex flex-col gap-2 mb-10">
<Heading as="h4">csv 파일 제출</Heading>
<p>하나의 csv 파일만 업로드 가능합니다</p>
<Input type="file" accept=".csv" />
const { mutate: createSubmission } = useCreateContestProblemSubmissionMutation();
const { mutate: createSubmissionCheck } = useCreateContestProblemSubmissionCheckMutation();

const handleFileSumbit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

<Heading as="h4">ipynb 파일 제출</Heading>
<p>하나의 ipynb 파일만 업로드 가능합니다</p>
<Input type="file" accept=".ipynb" />
const [csv, ipynb] = Object.values(e.target).map(({ files }) => files && files[0]);

<Button className="inline-block">파일 제출</Button>
</form>
const formData = new FormData();
formData.append('csv', csv);
formData.append('ipynb', ipynb);

<Heading as="h4">제출 내역</Heading>
<p>선택한 제출 내역이 리더보드에 표시됩니다.</p>
<Table column={column} data={data} />
createSubmission({ classId, contestId, contestProblemId, payload: formData });
};

const handleCheckSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const { id } = Object.values(e.target).find(({ checked }) => checked);
createSubmissionCheck({ classId, contestId, contestProblemId, payload: { id } });
};

return (
<>
<FileSubmissionForm onSubmit={handleFileSumbit} />
<LeaderboardSubmissionForm onSubmit={handleCheckSubmit} />
</>
);
}
42 changes: 41 additions & 1 deletion src/pages/Class/Problem/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,44 @@ const createProblem = (payload: FormData) => {
return fileApi.post(`${API_URL}/`, payload);
};

export { getProblem, createProblem };
const getContestProblem = ({ classId, contestId, contestProblemId }: ContestProblemRequest) => {
return api.get(`/class/${classId}/contests/${contestId}/${contestProblemId}`);
};

/** FIXME: 페이지네이션, username */
const getContestProblemSubmission = ({
classId,
contestId,
contestProblemId,
}: ContestProblemRequest) => {
return api.get(`/class/${classId}/contests/${contestId}/${contestProblemId}/submissions`);
};

const createContestProblemSubmission = ({
classId,
contestId,
contestProblemId,
payload,
}: ContestProblemRequest & { payload: FormData }) => {
return fileApi.post(
`/class/${classId}/contests/${contestId}/${contestProblemId}/submission/`,
payload
);
};

const createContestProblemSumbissionCheck = ({
classId,
contestId,
contestProblemId,
}: ContestProblemRequest) => {
return api.patch(`/class/${classId}/contests/${contestId}/${contestProblemId}/check/`);
};

export {
getProblem,
createProblem,
getContestProblem,
getContestProblemSubmission,
createContestProblemSubmission,
createContestProblemSumbissionCheck,
};
19 changes: 19 additions & 0 deletions src/pages/Class/Problem/components/FileSubmissionForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Heading, Input, Button } from '@/components';

type FileSubmissionFormProps<T extends React.ElementType> = Component<T>;

export function FileSubmissionForm({ ...props }: FileSubmissionFormProps<'form'>) {
return (
<form className="flex flex-col gap-2 mb-10" {...props}>
<Heading as="h4">csv 파일 제출</Heading>
<p>하나의 csv 파일만 업로드 가능합니다</p>
<Input type="file" accept=".csv" />

<Heading as="h4">ipynb 파일 제출</Heading>
<p>하나의 ipynb 파일만 업로드 가능합니다</p>
<Input type="file" accept=".ipynb" />

<Button className="inline-block">파일 제출</Button>
</form>
);
}
47 changes: 47 additions & 0 deletions src/pages/Class/Problem/components/LeaderboardSubmissionForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useParams } from 'react-router-dom';

import { Heading, Table, Button } from '@/components';
import { formatTime } from '@/utils/time';

import { useContestProblemSubmissionQuery } from '../hooks';

type FileSubmissionFormProps<T extends React.ElementType> = Component<T>;

export function LeaderboardSubmissionForm({ ...props }: FileSubmissionFormProps<'form'>) {
const { classId, contestId, contestProblemId } = useParams() as {
classId: string;
contestId: string;
contestProblemId: string;
};

const column = [
{ Header: 'check', accessor: 'check' },
{ Header: 'csv 파일', accessor: 'csvFile' },
{ Header: 'ipynb 파일', accessor: 'ipynbFile' },
{ Header: '점수', accessor: 'score' },
{ Header: 'status', accessor: 'status' },
{ Header: '제출 날짜', accessor: 'submissionDate' },
];

const {
data: { results },
} = useContestProblemSubmissionQuery({ classId, contestId, contestProblemId });

const data = results
.sort(({ score: prev }, { score: next }) => next - prev)
.sort(({ created_time: prev }, { created_time: next }) => +new Date(prev) - +new Date(next))
.map((submission) => ({
...submission,
check: <input type="checkbox" id={submission.id} />,
submissionDate: formatTime(submission.created_time),
}));

return (
<form className="flex flex-col gap-2 mb-10" {...props}>
<Heading as="h4">제출 내역</Heading>
<p>선택한 제출 내역이 리더보드에 표시됩니다.</p>
<Table column={column} data={data} />
<Button>제출</Button>
</form>
);
}
2 changes: 2 additions & 0 deletions src/pages/Class/Problem/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './Problem';
export * from './FileSubmissionForm';
export * from './LeaderboardSubmissionForm';
4 changes: 4 additions & 0 deletions src/pages/Class/Problem/hooks/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export * from './useProblemQuery';
export * from './useCreateProblemMutation';
export * from './useContestProblemQuery';
export * from './useContestProblemSubmissionQuery';
export * from './useCreateContestProblemSubmissionMutation';
export * from './useCreateContestProblemSubmissionCheckMutation';
24 changes: 24 additions & 0 deletions src/pages/Class/Problem/hooks/query/useContestProblemQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AxiosError } from 'axios';
import { UseQueryOptions } from 'react-query';

import { useSuspenseQuery } from '@/hooks/useSuspenseQuery';
import { QUERY_KEYS } from '@/constants';

import { getContestProblem } from '../../api';

export const useContestProblemQuery = (
params: ContestProblemRequest,
options?: UseQueryOptions<Problem, AxiosError, Problem, string[]>
) => {
const { classId, contestId, contestProblemId } = params;
return useSuspenseQuery(
[QUERY_KEYS.CLASS_CONTEST_PROBLEM, classId, contestId, contestProblemId],
async () => {
const { data } = await getContestProblem(params);
return data;
},
{
...options,
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AxiosError } from 'axios';
import { UseQueryOptions } from 'react-query';

import { useSuspenseQuery } from '@/hooks/useSuspenseQuery';
import { QUERY_KEYS } from '@/constants';

import { getContestProblemSubmission } from '../../api';

export const useContestProblemSubmissionQuery = (
params: ContestProblemRequest,
options?: UseQueryOptions<
ContestProblemSubmissionResponse,
AxiosError,
ContestProblemSubmissionResponse,
string[]
>
) => {
const { classId, contestId, contestProblemId } = params;
return useSuspenseQuery(
[QUERY_KEYS.CLASS_CONTEST_PROBLEM_SUBMISSION, classId, contestId, contestProblemId],
async () => {
const { data } = await getContestProblemSubmission(params);
return data;
},
{
...options,
}
);
};
Loading

0 comments on commit 5870054

Please sign in to comment.