Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Commit

Permalink
feat : detail scoreboard
Browse files Browse the repository at this point in the history
  • Loading branch information
kasterra committed May 7, 2024
1 parent b24bfd7 commit a65c60e
Show file tree
Hide file tree
Showing 12 changed files with 484 additions and 2 deletions.
2 changes: 1 addition & 1 deletion app/API/practice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PracticeDetailResponse } from "~/types/APIResponse";
const API_SERVER_URL = "http://155.230.34.223:53469/api/v1";

export async function getPracticeWithPracticeId(
practiceId: number,
practiceId: number | string,
token: string
): Promise<PracticeDetailResponse> {
const response = await fetch(`${API_SERVER_URL}/practice/${practiceId}`, {
Expand Down
59 changes: 59 additions & 0 deletions app/API/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,62 @@ export async function getSubmissionStatus(
}
return { ...(await response.json()), status: response.status };
}

export async function getLectureScoreBoard(
token: string,
lecture_id: string | number
) {
const response = await fetch(
`${API_SERVER_URL}/lecture/${lecture_id}/score`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
}
);
switch (response.status) {
case 400:
toast.error("JWT토큰이 없거나 입력값 검증 실패");
break;
case 401:
toast.error("유효하지 않은 JWT 토큰. 다시 로그인 하세요");
break;
case 403:
toast.error("소속되지 않은 강의의 스코어보드 접근");
break;
}
return { ...(await response.json()), status: response.status };
}

export async function getPracticeScoreBoard(
token: string,
practice_id: number | string
) {
const response = await fetch(
`${API_SERVER_URL}/practice/${practice_id}/score`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
}
);
switch (response.status) {
case 400:
toast.error("JWT토큰이 없거나 입력값 검증 실패");
break;
case 401:
toast.error("유효하지 않은 JWT 토큰. 다시 로그인 하세요");
break;
case 403:
toast.error("소속되지 않은 강의의 스코어보드 접근");
break;
case 404:
toast.error("존재하지 않는 강의");
break;
}
return { ...(await response.json()), status: response.status };
}
3 changes: 3 additions & 0 deletions app/components/Input/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Props {
defaultValue?: string;
width?: number;
height?: number;
disabled?: boolean;
}

const TextArea = ({
Expand All @@ -20,6 +21,7 @@ const TextArea = ({
defaultValue,
width,
height,
disabled = false,
}: Props) => {
return (
<div className={styles.wrapper}>
Expand All @@ -33,6 +35,7 @@ const TextArea = ({
placeholder={placeholder}
required={required}
defaultValue={defaultValue}
disabled={disabled}
style={{
width: width ? width : undefined,
height: height ? height : undefined,
Expand Down
84 changes: 84 additions & 0 deletions app/routes/_procted+/grade+/$lectureId+/$practiceId+/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useParams } from "@remix-run/react";
import { ReactNode, useEffect, useState } from "react";
import { getPracticeWithPracticeId } from "~/API/practice";
import { getPracticeScoreBoard } from "~/API/submission";
import { useAuth } from "~/contexts/AuthContext";
import styles from "../index.module.css";
import TableBase from "~/components/Table/TableBase";

const PracticeScoreBoard = () => {
const params = useParams();
const auth = useAuth();
const [practiceName, setPracticeName] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState<any>({});
const [dataHeaders, setDataHeaders] = useState<ReactNode[]>(["이름", "총점"]);

useEffect(() => {
async function getPracticeName() {
const response = await getPracticeWithPracticeId(
params.practiceId!,
auth.token
);
if (response.status === 200) {
setPracticeName((response as any).data.name);
}
}
getPracticeName();
}, []);

useEffect(() => {
async function getPracticeScore() {
const response = await getPracticeScoreBoard(
auth.token,
params.practiceId!
);
if (response.status === 200) {
response.data.metadata.map((data: any) => {
setDataHeaders((prev) => [
...prev,
<button
className={styles["white-button"]}
onClick={() => alert("재채점 API호출 예정")}
>{`${data.title} (${data.score})`}</button>,
]);
});
}
setData(
response.data.users.map((user: any) => {
const map = new Map<string, ReactNode>();
map.set("userName", user.name);
map.set("totalScore", user.total_score);
user.scores.map((score: any, idx: number) => {
map.set(
`problemNo${idx}`,
<button
className={styles["white-button"]}
onClick={() => alert("문제 재채점 API호출 예정")}
>
{score}
</button>
);
});
return map;
})
);
setIsLoading(false);
}
getPracticeScore();
}, []);

return isLoading ? (
<h2>Loading...</h2>
) : (
<TableBase
gridTemplateColumns={`150px 150px ${"200px ".repeat(
dataHeaders.length
)} `}
dataHeaders={dataHeaders as ReactNode[]}
dataRows={data}
/>
);
};

export default PracticeScoreBoard;
50 changes: 50 additions & 0 deletions app/routes/_procted+/grade+/$lectureId+/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import styles from "./index.module.css";
import { Outlet, useParams } from "@remix-run/react";
import { getLectureWithLectureId } from "~/API/lecture";
import { useAuth } from "~/contexts/AuthContext";
import { ButtonElement } from "~/components/Aside/ButtonElement";
import { Link } from "react-router-dom";

const ScoreBoardLayout = () => {
const params = useParams();
const auth = useAuth();
const [isLoading, setIsLoading] = useState(true);
const [practiceList, setPracticeList] = useState([]);
useEffect(() => {
async function getPractices() {
const response = await getLectureWithLectureId(
params.lectureId!,
auth.token
);
if (response.status === 200) {
setPracticeList((response as any).data.practices);
setIsLoading(false);
}
}
getPractices();
}, []);
return isLoading ? (
<h1>Loading...</h1>
) : (
<div className={styles.container}>
<div className={styles.sidebar}>
<Link to={`/grade/${params.lectureId}`}>
<ButtonElement title={"실습 전체 보기"} onButtonClick={() => {}} />
</Link>
{practiceList.map((practice: any, idx: number) => (
<Link to={`/grade/${params.lectureId}/${practice.id}`}>
<ButtonElement
title={practice.title}
key={practice.id}
onButtonClick={() => {}}
/>
</Link>
))}
</div>
<Outlet />
</div>
);
};

export default ScoreBoardLayout;
27 changes: 27 additions & 0 deletions app/routes/_procted+/grade+/$lectureId+/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.white-button {
all: unset;
cursor: pointer;
padding: 10px 16px;
border-radius: 8px;
border: 1px solid #d0d5dd;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
}

.container {
display: flex;
gap: 40px;
}

.sidebar {
display: flex;
flex-direction: column;
}

.sidebar * {
text-decoration: none;
}
Loading

0 comments on commit a65c60e

Please sign in to comment.