diff --git a/app/routes/_procted+/lectures+/quiz+/_layout.tsx b/app/routes/_procted+/lectures+/quiz+/_layout.tsx new file mode 100644 index 0000000..da3e309 --- /dev/null +++ b/app/routes/_procted+/lectures+/quiz+/_layout.tsx @@ -0,0 +1,27 @@ +import { useAuth } from "~/contexts/AuthContext"; +import { useEffect } from "react"; +import toast from "react-hot-toast"; +import { Outlet, useNavigate, useSearchParams } from "@remix-run/react"; + +const Wrapper = () => { + const auth = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + useEffect(() => { + if (!auth.isRoleFetching && auth.role !== "professor") { + toast.error("교수 전용 페이지입니다. 교수 계정으로 로그인 하세요"); + navigate("/"); + } + }, []); + useEffect(() => { + if (!searchParams.get("lecture_id") || !searchParams.get("practice_id")) { + toast.error( + "강의 ID와 실습 ID 정보가 없습니다.\n\n잘못된 접근인것 같습니다" + ); + navigate("/lectures"); + } + }, []); + return ; +}; + +export default Wrapper; diff --git a/app/routes/_procted+/lectures+/quiz+/index.module.css b/app/routes/_procted+/lectures+/quiz+/index.module.css new file mode 100644 index 0000000..9f4a012 --- /dev/null +++ b/app/routes/_procted+/lectures+/quiz+/index.module.css @@ -0,0 +1,81 @@ +.top-container { + display: flex; + justify-content: center; +} + +.container { + display: flex; + flex-direction: column; + gap: 110px; +} + +.form-area { + display: flex; + flex-direction: column; + gap: 30px; + align-items: center; + margin-bottom: 50px; +} + +.text-area { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + color: #667085; + font-size: 16px; +} + +.data-input-area { + display: flex; + flex-direction: column; + gap: 10px; +} + +.file-input-area { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.table-area { + display: flex; + align-items: center; + gap: 10px; +} + +.table-manage-btns { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 14px; +} + +.btn { + all: unset; + cursor: pointer; + width: 30px; + height: 30px; +} + +.btn img { + width: 100%; + height: 100%; +} + +.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; +} diff --git a/app/routes/_procted+/lectures+/quiz+/new/index.tsx b/app/routes/_procted+/lectures+/quiz+/new/index.tsx new file mode 100644 index 0000000..4d86c28 --- /dev/null +++ b/app/routes/_procted+/lectures+/quiz+/new/index.tsx @@ -0,0 +1,256 @@ +import TextInput from "~/components/Input/TextInput"; +import styles from "../index.module.css"; +import judgeStyles from "~/css/judge.module.css"; +import inputStyles from "~/components/Input/input.module.css"; +import formStyles from "~/components/common/form.module.css"; +import { MetaFunction, useSearchParams } from "@remix-run/react"; +import { getUsersInLecture } from "~/API/lecture"; +import { ReactNode, useEffect, useState } from "react"; +import { useAuth } from "~/contexts/AuthContext"; +import { UserEntity } from "~/types/APIResponse"; +import TableBase from "~/components/Table/TableBase"; +import plusSquare from "~/assets/plus-square.svg"; +import minusSquare from "~/assets/minus-square.svg"; +import SingleFileInput from "~/components/Input/SingleFileInput"; +import fileDownloadSVG from "~/assets/fileDownload.svg"; +import { createQuizResultXlsx, parseQuizResultXlsx } from "~/util/xlsx"; +import pkg from "file-saver"; +import toast from "react-hot-toast"; +const { saveAs } = pkg; + +const QuizRegister = () => { + const auth = useAuth(); + const [loading, setLoading] = useState(true); + const [searchParams] = useSearchParams(); + const lecture_id = searchParams.get("lecture_id")!; + const practice_id = searchParams.get("practice_id")!; + const [users, setUsers] = useState([]); + const [tableData, setTableData] = useState[]>([]); + const [dataHeaders, setDataHeaders] = useState([ + "학생 명", + "Q1", + ]); + useEffect(() => { + async function getData() { + const { data: users } = await getUsersInLecture(lecture_id, auth.token); + setUsers(users); + setLoading(false); + } + getData(); + }, []); + useEffect(() => { + setTableData([ + (function () { + const map = new Map(); + map.set( + "학생 명", + 문제별 만점 + ); + map.set( + "Q1", + + ); + return map; + })(), + ...users.map((user, userIndex) => { + const map = new Map(); + map.set("학생 명", user.name); + map.set( + "Q1", + + ); + return map; + }), + ]); + }, [users]); + + return loading ? ( +

loading...

+ ) : ( +
+
+
+

퀴즈 성적 등록

+

퀴즈 성적을 등록합니다

+
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const data: { + [studentId: string]: { [questionId: string]: string }; + } = {}; + + for (let [key, value] of formData.entries()) { + if (key === "xlsx") continue; + const [studentId, questionId] = key.split("-"); + if (!data[studentId]) { + data[studentId] = {}; + } + data[studentId][questionId] = value as string; + } + + console.log(data); + }} + > +
+ +, - 버튼을 이용해서 문제를 추가/삭제 할 수 있습니다 + 결석 등 점수가 없으면 n을 입력해 주세요 +
+
+
+ { + const result = await toast.promise( + parseQuizResultXlsx(file), + { + loading: "xlsx 파일 읽는중...", + success: "성공적으로 읽었습니다!", + error: (err) => + `Error: ${err.message} - ${err.responseMessage}`, + } + ); + setTableData((prev) => + result.map((userScoreRow, userIndex) => { + const map = new Map(); + map.set("학생 명", prev[userIndex].get("학생 명")); + userScoreRow.map((userScore, scoreIdx) => { + map.set( + `Q${scoreIdx + 1}`, + + ); + }); + return map; + }) + ); + setDataHeaders([ + "학생 명", + ...Array.from({ length: result[0].length }).map( + (_, i) => `Q${i + 1}` + ), + ]); + }} + /> + +
+
+ +
+ + +
+
+
+ +
+
+
+ ); +}; + +export default QuizRegister; + +export const meta: MetaFunction = () => { + return [ + { + title: "퀴즈 성적 입력 | KOJ", + }, + { + property: "description", + content: "퀴즈 성적 입력 화면입니다", + }, + { + property: "og:site_name", + content: "KOJ - 퀴즈 성적 입력", + }, + ]; +}; diff --git a/app/util/xlsx.ts b/app/util/xlsx.ts index 3c3b21e..2a7c672 100644 --- a/app/util/xlsx.ts +++ b/app/util/xlsx.ts @@ -1,5 +1,6 @@ import ExcelJS from "exceljs"; import { studentRow } from "~/types"; +import { UserEntity } from "~/types/APIResponse"; export async function parseLectureMemberXlsx( file: File @@ -37,3 +38,54 @@ export async function parseLectureMemberXlsx( } return []; } + +export async function createQuizResultXlsx(users: UserEntity[]) { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Sheet1", { + properties: { + defaultColWidth: 10, + }, + }); + + worksheet.getCell("A1").value = "▶ 서식에서 열은 자유롭게 추가 가능합니다"; + worksheet.getCell("A4").value = "학생 명"; + worksheet.getCell("B4").value = "Q1"; + + worksheet.getCell("A5").value = "문제별 만점"; + for (let i = 0; i < users.length; ++i) { + worksheet.getCell(`A${6 + i}`).value = users[i].name; + } + + return new File([await workbook.xlsx.writeBuffer()], "quizResultForm.xlsx"); +} + +export async function parseQuizResultXlsx(file: File) { + let result: (number | "n")[][] = []; + try { + const workbook = new ExcelJS.Workbook(); + const buffer = await file.arrayBuffer(); + await workbook.xlsx.load(buffer); + const worksheet = workbook.worksheets[0]; + + const numberOfProblems = worksheet.columnCount - 1; + const numberOfDataRow = worksheet.rowCount - 4; + + result = Array.from({ length: numberOfDataRow }, () => + Array.from({ length: numberOfProblems }) + ); + + for (let i = 1; i <= numberOfProblems; ++i) { + const curColumn = worksheet.getColumn(i + 1); + const scoreList = curColumn.values.filter( + (val) => val === "n" || typeof val === "number" + ) as (number | "n")[]; + + scoreList.forEach((val, idx) => { + result[idx][i - 1] = val; + }); + } + } catch (error) { + console.error(error); + } + return result; +}