diff --git a/app/API/practice.ts b/app/API/practice.ts index 6d9e1b4..a2b75b8 100644 --- a/app/API/practice.ts +++ b/app/API/practice.ts @@ -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 { const response = await fetch(`${API_SERVER_URL}/practice/${practiceId}`, { diff --git a/app/API/submission.ts b/app/API/submission.ts index a8d1a6e..5f70d0f 100644 --- a/app/API/submission.ts +++ b/app/API/submission.ts @@ -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 }; +} diff --git a/app/components/Input/TextArea.tsx b/app/components/Input/TextArea.tsx index ae67dc1..7822fe3 100644 --- a/app/components/Input/TextArea.tsx +++ b/app/components/Input/TextArea.tsx @@ -9,6 +9,7 @@ interface Props { defaultValue?: string; width?: number; height?: number; + disabled?: boolean; } const TextArea = ({ @@ -20,6 +21,7 @@ const TextArea = ({ defaultValue, width, height, + disabled = false, }: Props) => { return (
@@ -33,6 +35,7 @@ const TextArea = ({ placeholder={placeholder} required={required} defaultValue={defaultValue} + disabled={disabled} style={{ width: width ? width : undefined, height: height ? height : undefined, diff --git a/app/routes/_procted+/grade+/$lectureId+/$practiceId+/index.tsx b/app/routes/_procted+/grade+/$lectureId+/$practiceId+/index.tsx new file mode 100644 index 0000000..bb0b708 --- /dev/null +++ b/app/routes/_procted+/grade+/$lectureId+/$practiceId+/index.tsx @@ -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({}); + const [dataHeaders, setDataHeaders] = useState(["이름", "총점"]); + + 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, + , + ]); + }); + } + setData( + response.data.users.map((user: any) => { + const map = new Map(); + map.set("userName", user.name); + map.set("totalScore", user.total_score); + user.scores.map((score: any, idx: number) => { + map.set( + `problemNo${idx}`, + + ); + }); + return map; + }) + ); + setIsLoading(false); + } + getPracticeScore(); + }, []); + + return isLoading ? ( +

Loading...

+ ) : ( + + ); +}; + +export default PracticeScoreBoard; diff --git a/app/routes/_procted+/grade+/$lectureId+/_layout.tsx b/app/routes/_procted+/grade+/$lectureId+/_layout.tsx new file mode 100644 index 0000000..a5c25e4 --- /dev/null +++ b/app/routes/_procted+/grade+/$lectureId+/_layout.tsx @@ -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 ? ( +

Loading...

+ ) : ( +
+
+ + {}} /> + + {practiceList.map((practice: any, idx: number) => ( + + {}} + /> + + ))} +
+ +
+ ); +}; + +export default ScoreBoardLayout; diff --git a/app/routes/_procted+/grade+/$lectureId+/index.module.css b/app/routes/_procted+/grade+/$lectureId+/index.module.css new file mode 100644 index 0000000..846549f --- /dev/null +++ b/app/routes/_procted+/grade+/$lectureId+/index.module.css @@ -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; +} diff --git a/app/routes/_procted+/grade+/$lectureId+/index.tsx b/app/routes/_procted+/grade+/$lectureId+/index.tsx new file mode 100644 index 0000000..28003df --- /dev/null +++ b/app/routes/_procted+/grade+/$lectureId+/index.tsx @@ -0,0 +1,195 @@ +import { useNavigate, useParams } from "@remix-run/react"; +import { ReactNode, useEffect, useRef, useState } from "react"; +import { getLectureScoreBoard } from "~/API/submission"; +import TableBase from "~/components/Table/TableBase"; +import chevUpSVG from "~/assets/chevronUp.svg"; +import chevDownSVG from "~/assets/chevronDown.svg"; +import { useAuth } from "~/contexts/AuthContext"; +import { + useLectureData, + useLectureDataDispatch, +} from "~/contexts/LectureDataContext"; +import { Lecture } from "~/types"; +import dropdownStyles from "~/components/common/dropdown.module.css"; +import styles from "./index.module.css"; +import headerStyles from "~/routes/_procted+/students+/$lectureId+/history+/index.module.css"; +import { + getCurrentSemesterLectures, + getPreviousSemesterLectures, + getFutureSemesterLectures, +} from "~/API/lecture"; +import { + isSuccessResponse, + SuccessLecturesResponse, +} from "~/types/APIResponse"; + +const TableHeader = () => { + const navigate = useNavigate(); + const auth = useAuth(); + const lectureData = useLectureData(); + const dispatchLectureData = useLectureDataDispatch(); + const [lectureListLoading, setLectureListLoading] = useState(true); + const [lectureList, setLectureList] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const getLectureList = async () => { + try { + if (lectureData.semester === "present") { + const response = await getCurrentSemesterLectures( + auth.userId, + auth.token + ); + if (isSuccessResponse(response)) + setLectureList((response as SuccessLecturesResponse).data); + } else if (lectureData.semester === "past") { + const response = await getPreviousSemesterLectures( + auth.userId, + auth.token + ); + if (isSuccessResponse(response)) + setLectureList((response as SuccessLecturesResponse).data); + } else if (lectureData.semester === "future") { + const response = await getFutureSemesterLectures( + auth.userId, + auth.token + ); + if (isSuccessResponse(response)) + setLectureList((response as SuccessLecturesResponse).data); + } + setLectureListLoading(false); + } catch (error) { + console.error(error); + } + }; + getLectureList(); + }, [lectureData.semester]); + + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [containerRef]); + + return ( +
+ + + {isOpen ? ( +
+ {lectureListLoading + ? "강의목록 로딩중..." + : lectureList.map((lecture) => ( +
{ + dispatchLectureData({ + type: "UPDATE_DATA", + payload: { + lectureName: lecture.title, + semester: lectureData.semester, + }, + }); + navigate(`/students/${lecture.id}`); + }} + > + {lecture.title} +
+ ))} +
+ ) : null} +
+ ); +}; + +const LectureScoreBoard = () => { + const auth = useAuth(); + const params = useParams(); + const [isLoading, setIsLoading] = useState(true); + const [data, setData] = useState({}); + const [dataHeaders, setDataHeaders] = useState([ + "사용자 이름", + "학번", + "총점", + ]); + + useEffect(() => { + async function getData() { + const response = await getLectureScoreBoard( + auth.token, + params.lectureId! + ); + if (response.status === 200) { + response.data.metadata.map((data: any) => { + console.log(data); + setDataHeaders((prev) => [ + ...prev, + , + ]); + }); + setData( + response.data.users.map((user: any) => { + const map = new Map(); + map.set("userName", user.name); + map.set("userId", user.id); + map.set("totalScore", user.total_score); + user.scores.map((score: any, idx: number) => { + map.set( + `problemNo${idx}`, + `${ + score === response.data.metadata[idx].score + ? "○" + : score > 0 + ? "△" + : "✕" + } (${score})` + ); + }); + return map; + }) + ); + setIsLoading(false); + } + } + getData(); + }, []); + + return isLoading ? ( +

Loading...

+ ) : ( + + ); +}; + +export default LectureScoreBoard; diff --git a/app/routes/_procted+/grade+/index.tsx b/app/routes/_procted+/grade+/index.tsx new file mode 100644 index 0000000..dfaa1f7 --- /dev/null +++ b/app/routes/_procted+/grade+/index.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "@remix-run/react"; +import { useEffect } from "react"; +import { getCurrentSemesterLectures } from "~/API/lecture"; +import { useAuth } from "~/contexts/AuthContext"; +import { + SuccessLecturesResponse, + isSuccessResponse, +} from "~/types/APIResponse"; + +const GradeRedirect = () => { + const navigate = useNavigate(); + const auth = useAuth(); + useEffect(() => { + async function getLectures() { + const response = await getCurrentSemesterLectures( + auth.userId, + auth.token + ); + if (isSuccessResponse(response)) { + navigate(`/grade/${(response as SuccessLecturesResponse).data[0].id}`); + } + } + getLectures(); + }, []); + + return ( +

+ 내 강의로 리다이렉트중.... +
이 화면이 계속 나온다면, 이번 학기에 강의중인 강의가 있는지 확인해 + 보세요 +

+ ); +}; + +export default GradeRedirect; diff --git a/app/routes/_procted+/lectures+/$lectureId+/$labId/SubmitModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/$labId/SubmitModal.tsx index 2596527..40a0b6b 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/$labId/SubmitModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/$labId/SubmitModal.tsx @@ -39,6 +39,7 @@ const SubmitModal = ({ isOpen, onClose }: Props) => { if (fileList) { [...fileList].forEach((file) => formData.append("codes", file)); } + formData.append("code", code); await submit(auth.token, labId!, formData); navigate(`/students/${labId}/history`); }} diff --git a/app/routes/_procted+/lectures+/$lectureId+/$labId/index.module.css b/app/routes/_procted+/lectures+/$lectureId+/$labId/index.module.css index 4976ca8..7193716 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/$labId/index.module.css +++ b/app/routes/_procted+/lectures+/$lectureId+/$labId/index.module.css @@ -140,4 +140,5 @@ .flex { display: flex; gap: 50px; + flex-direction: column; } diff --git a/app/routes/_procted+/students+/$lectureId+/history+/SubmissionDetailModal.tsx b/app/routes/_procted+/students+/$lectureId+/history+/SubmissionDetailModal.tsx index 896ebbb..d55fcb4 100644 --- a/app/routes/_procted+/students+/$lectureId+/history+/SubmissionDetailModal.tsx +++ b/app/routes/_procted+/students+/$lectureId+/history+/SubmissionDetailModal.tsx @@ -5,6 +5,7 @@ import styles from "./index.module.css"; import { useAuth } from "~/contexts/AuthContext"; import CodeBlock from "~/components/CodeBlock"; import TextInput from "~/components/Input/TextInput"; +import TextArea from "~/components/Input/TextArea"; interface Props { isOpen: boolean; @@ -113,6 +114,18 @@ const SubmissionDetailModal = ({ isOpen, onClose, submissionId }: Props) => { } })()}
+ + { name="" defaultValue={result.judge_answer} /> + + {result.message && ( +