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

Commit

Permalink
feat(quiz) : Quiz result submit UI
Browse files Browse the repository at this point in the history
  • Loading branch information
kasterra committed May 28, 2024
1 parent 4c12e2e commit 820bbe4
Show file tree
Hide file tree
Showing 4 changed files with 416 additions and 0 deletions.
27 changes: 27 additions & 0 deletions app/routes/_procted+/lectures+/quiz+/_layout.tsx
Original file line number Diff line number Diff line change
@@ -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 <Outlet />;
};

export default Wrapper;
81 changes: 81 additions & 0 deletions app/routes/_procted+/lectures+/quiz+/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
256 changes: 256 additions & 0 deletions app/routes/_procted+/lectures+/quiz+/new/index.tsx
Original file line number Diff line number Diff line change
@@ -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<UserEntity[]>([]);
const [tableData, setTableData] = useState<Map<string, ReactNode>[]>([]);
const [dataHeaders, setDataHeaders] = useState<ReactNode[]>([
"학생 명",
"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<string, ReactNode>();
map.set(
"학생 명",
<span className={judgeStyles.correct}>문제별 만점</span>
);
map.set(
"Q1",
<input
className={inputStyles.input}
required
placeholder="만점 점수"
name="S0-Q1"
/>
);
return map;
})(),
...users.map((user, userIndex) => {
const map = new Map<string, ReactNode>();
map.set("학생 명", user.name);
map.set(
"Q1",
<input
className={inputStyles.input}
required
placeholder="점수 입력"
name={`S${userIndex + 1}-Q1`}
/>
);
return map;
}),
]);
}, [users]);

return loading ? (
<h3>loading...</h3>
) : (
<div className={styles["top-container"]}>
<div className={styles.container}>
<div className={formStyles["title-area"]}>
<h1 className={formStyles.title}>퀴즈 성적 등록</h1>
<h2 className={formStyles.subtitle}>퀴즈 성적을 등록합니다</h2>
</div>
<form
className={styles["form-area"]}
onSubmit={(e) => {
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);
}}
>
<div className={styles["text-area"]}>
<span>+, - 버튼을 이용해서 문제를 추가/삭제 할 수 있습니다</span>
<span>결석 등 점수가 없으면 n을 입력해 주세요</span>
</div>
<div className={styles["data-input-area"]}>
<div className={styles["file-input-area"]}>
<SingleFileInput
title="채점 결과 xlsx 파일"
name="xlsx"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
onFileUpload={async (file) => {
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<string, ReactNode>();
map.set("학생 명", prev[userIndex].get("학생 명"));
userScoreRow.map((userScore, scoreIdx) => {
map.set(
`Q${scoreIdx + 1}`,
<input
className={inputStyles.input}
required
placeholder={
userIndex == 0 ? "만점 점수" : "점수 입력"
}
defaultValue={userScore}
name={`S${userIndex}-Q${scoreIdx + 1}`}
/>
);
});
return map;
})
);
setDataHeaders([
"학생 명",
...Array.from({ length: result[0].length }).map(
(_, i) => `Q${i + 1}`
),
]);
}}
/>
<button
className={styles["white-button"]}
type="button"
onClick={async () => {
saveAs(await createQuizResultXlsx(users));
}}
>
<img src={fileDownloadSVG} alt="파일 다운로드" />
<span>엑셀 서식 다운로드</span>
</button>
</div>
<div className={styles["table-area"]}>
<TableBase
gridTemplateColumns={`140px ${"150px ".repeat(
dataHeaders.length
)}`}
dataHeaders={dataHeaders}
dataRows={tableData}
/>
<div className={styles["table-manage-btns"]}>
<button
type="button"
className={styles.btn}
onClick={() => {
setTableData((prev) =>
prev.map((data: Map<string, ReactNode>, idx) => {
if (idx === 0) {
data.set(
`Q${dataHeaders.length}`,
<input
className={inputStyles.input}
required
placeholder="만점 점수"
name={`S0-Q${dataHeaders.length}`}
></input>
);
return data;
}
data.set(
`Q${dataHeaders.length}`,
<input
className={inputStyles.input}
required
placeholder="점수 입력"
name={`S${idx}-Q${dataHeaders.length}`}
></input>
);
return data;
})
);
setDataHeaders([...dataHeaders, `Q${dataHeaders.length}`]);
}}
>
<img src={plusSquare} alt="plus btn" />
</button>
<button
type="button"
className={styles.btn}
onClick={() => {
if (dataHeaders.length === 2) return;
setTableData((prev) => {
prev.map((elem) => {
elem.delete(`Q${dataHeaders.length - 1}`);
return elem;
});
return prev;
});
setDataHeaders((prev) => prev.slice(0, -1));
}}
>
<img src={minusSquare} alt="minus btn" />
</button>
</div>
</div>
</div>
<button type="submit" className={formStyles["primary-button"]}>
저장하기
</button>
</form>
</div>
</div>
);
};

export default QuizRegister;

export const meta: MetaFunction = () => {
return [
{
title: "퀴즈 성적 입력 | KOJ",
},
{
property: "description",
content: "퀴즈 성적 입력 화면입니다",
},
{
property: "og:site_name",
content: "KOJ - 퀴즈 성적 입력",
},
];
};
Loading

0 comments on commit 820bbe4

Please sign in to comment.