From 02ec5dd75eff179ac4d1fbb25417c6d59065ec38 Mon Sep 17 00:00:00 2001 From: kasterra Date: Sun, 26 May 2024 17:18:58 +0900 Subject: [PATCH] feat : class implementation problem --- app/API/problem.ts | 72 ++++++++++- .../$practiceId+/$labId/SubmitModal.tsx | 2 +- .../$lectureId+/_layout/ProblemAddModal.tsx | 99 ++++++++++++-- .../$lectureId+/_layout/ProblemEditModal.tsx | 121 +++++++++++++++++- app/types/APIResponse.ts | 6 +- app/util/index.ts | 41 ++++++ 6 files changed, 322 insertions(+), 19 deletions(-) diff --git a/app/API/problem.ts b/app/API/problem.ts index 3a4a750..03bf793 100644 --- a/app/API/problem.ts +++ b/app/API/problem.ts @@ -107,8 +107,67 @@ export async function postBlankProblem( return await response.json(); } +export async function postClassImplementationProblem( + file: File, + memory_limit: number, + prepared_main: { content: string; name: string; language: string }, + language: string, + practice_id: number, + time_limit: number, + title: string, + token: string +): Promise { + if (0 > memory_limit || memory_limit > 2048) { + throw new BadRequestError("메모리 제한은 0 ~ 2048 사이 값을 넣어야 합니다"); + } + if (!title) { + throw new BadRequestError("제목은 필수 입력 필드입니다"); + } + if (0 > time_limit || time_limit > 10000) { + throw new BadRequestError("시간 제한은 0~10,000 사이의 값을 넣어야 합니다"); + } + if (!prepared_main.content) { + throw new BadRequestError("Main 파일은 반드시 있어야 합니다"); + } + const fileUploadResponse = await uploadFile(file, token); + const file_path = fileUploadResponse.data.path; + const response = await fetch(`${API_SERVER_URL}/problem`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + file_path, + memory_limit, + prepared_main: { + code: { + name: prepared_main.name, + content: prepared_main.content, + }, + language: prepared_main.language, + }, + practice_id, + time_limit, + title, + type: "class_implementation", + }), + }); + switch (response.status) { + case 400: + throw new BadRequestError("입력값 검증 실패"); + break; + case 401: + handle401(); + break; + case 403: + throw new ForbiddenError("강의 소유 권한이 없습니다. 다시 확인해 주세요"); + } + return await response.json(); +} + export async function updateProblem( - problemType: "solving" | "blank", + problemType: "solving" | "blank" | "class_implementation", problemId: number, memory_limit: number, time_limit: number, @@ -116,7 +175,8 @@ export async function updateProblem( token: string, file_path: string, parsed_code_elements?: parsedCodeElement[][], - language?: string + language?: string, + prepared_main?: { content: string; name: string; language: string } ): Promise { if (0 > memory_limit || memory_limit > 2048) { throw new BadRequestError("메모리 제한은 0 ~ 2048 사이 값을 넣어야 합니다"); @@ -132,7 +192,13 @@ export async function updateProblem( throw new BadRequestError("빈칸 문제에는 빈칸정보가 필요합니다"); } } - console.log(parsed_code_elements); + if (problemType === "class_implementation") { + if (!prepared_main) { + throw new BadRequestError( + "클래스/함수 구현 문제에는 Main 파일이 필요합니다" + ); + } + } const response = await fetch(`${API_SERVER_URL}/problem/${problemId}`, { method: "PUT", headers: { diff --git a/app/routes/_procted+/lectures+/$lectureId+/$practiceId+/$labId/SubmitModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/$practiceId+/$labId/SubmitModal.tsx index c5b0720..0de738a 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/$practiceId+/$labId/SubmitModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/$practiceId+/$labId/SubmitModal.tsx @@ -162,7 +162,7 @@ const SubmitModal = ({ isOpen, onClose }: Props) => { diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx index ed84e48..ae564e9 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx @@ -8,10 +8,19 @@ import SingleFileInput from "~/components/Input/SingleFileInput"; import CodeBlock from "~/components/CodeBlock"; import { codeHole, parsedCodeElement } from "~/util/codeHole"; import toast from "react-hot-toast"; -import { postBlankProblem, postSolveProblem } from "~/API/problem"; +import { + postBlankProblem, + postClassImplementationProblem, + postSolveProblem, +} from "~/API/problem"; import { useAuth } from "~/contexts/AuthContext"; import BlankPreviewModal from "./BlankPreviewModal"; import { lanugage } from "~/types"; +import { + getCodeFileExtension, + problemTitles, + readFileAsServerFormat, +} from "~/util"; interface Props { lectureName: string; @@ -28,19 +37,19 @@ const ProblemAddModal = ({ isOpen, onClose, }: Props) => { - const [problemType, setProblemType] = useState<"blank" | "solving">( - "solving" - ); + const [problemType, setProblemType] = useState< + "blank" | "solving" | "class_implementation" + >("solving"); const [language, setLanguage] = useState("c"); const [codeString, setCodeString] = useState(""); const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); const auth = useAuth(); const [dragFile, setDragFile] = useState(null); + + const [mainFile, setMainFile] = useState(null); return ( { + onClose(); + return "문제를 성공적으로 추가했습니다!"; + }, + error: (err) => + `Error: ${err.message} - ${err.responseMessage}`, + } + ); + break; } }} > @@ -120,8 +165,8 @@ const ProblemAddModal = ({ void} /> ) : null} + {problemType === "class_implementation" ? ( +
+ + + 언어에 맞는 Main 파일을 준비해 주세요. + + 직접 입력하거나(이 경우엔 파일 이름은 + "Main.언어에_맞는_확장자"로 설정됨) 파일을 업로드 해주세요 + + { + setMainFile(file); + }} + /> + {!mainFile && ( + + )} +
+ ) : null} diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx index 96cf0d8..fb6e98e 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx @@ -1,6 +1,7 @@ import Modal from "~/components/Modal"; import styles from "./modal.module.css"; import formStyles from "~/components/common/form.module.css"; +import judgeStyles from "~/css/judge.module.css"; import { useEffect, useState } from "react"; import { getProblemWithProblemId } from "~/API/lecture"; import { useAuth } from "~/contexts/AuthContext"; @@ -20,6 +21,10 @@ import SingleFileInput from "~/components/Input/SingleFileInput"; import { uploadFile } from "~/API/media"; import { lanugage } from "~/types"; import { STATIC_SERVER_URL } from "~/util/constant"; +import { getCodeFileExtension, readFileAsServerFormat } from "~/util"; +import download from "~/assets/download.svg"; +import pkg from "file-saver"; +const { saveAs } = pkg; interface Props { isOpen: boolean; @@ -29,14 +34,15 @@ interface Props { const ProblemEditModal = ({ isOpen, onClose, editingProblemId }: Props) => { const [loading, setLoading] = useState(true); - const [problemType, setProblemType] = useState<"blank" | "solving">( - "solving" - ); + const [problemType, setProblemType] = useState< + "blank" | "solving" | "class_implementation" + >("solving"); const [prevProblemInfo, setPrevProblemInfo] = useState(); const [language, setLanguage] = useState("c"); const [codeString, setCodeString] = useState(""); const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); const [dragFile, setDragFile] = useState(null); + const [mainFile, setMainFile] = useState(null); const auth = useAuth(); useEffect(() => { async function getData() { @@ -145,6 +151,48 @@ const ProblemEditModal = ({ isOpen, onClose, editingProblemId }: Props) => { ); onClose(); break; + case "class_implementation": + let mainFileObj: { + content: string; + name: string; + } = {} as { content: string; name: string }; + if (mainFile) { + mainFileObj = await readFileAsServerFormat(mainFile); + } + await toast.promise( + updateProblem( + problemType, + editingProblemId, + memory, + time, + name, + auth.token, + newFilePath.length + ? newFilePath + : prevProblemInfo!.file_path, + undefined, + undefined, + mainFile + ? { ...mainFileObj, language } + : codeString + ? { + content: codeString, + name: `Main.${getCodeFileExtension(language)}`, + language, + } + : prevProblemInfo!.prepared_main + ? { + ...prevProblemInfo!.prepared_main.code, + language: prevProblemInfo!.prepared_main.language, + } + : undefined + ), + { + loading: "문제를 수정하는중...", + success: "문제를 성공적으로 수정했습니다!", + error: (e) => `Error: ${e.message} - ${e.responseMessage}`, + } + ); } }} > @@ -160,8 +208,8 @@ const ProblemEditModal = ({ isOpen, onClose, editingProblemId }: Props) => { void} defaultValue={prevProblemInfo!.type} /> @@ -244,6 +292,69 @@ const ProblemEditModal = ({ isOpen, onClose, editingProblemId }: Props) => { )} ) : null} + {problemType === "class_implementation" ? ( +
+ + + 언어에 맞는 Main 파일을 준비해 주세요. + + 직접 입력하거나(이 경우엔 파일 이름은 + "Main.언어에_맞는_확장자"로 설정됨) 파일을 업로드 해주세요 + + {prevProblemInfo!.prepared_main && ( + <> +

기존 main 파일

+
{ + saveAs( + new File( + [prevProblemInfo!.prepared_main.code.content], + prevProblemInfo!.prepared_main.code.name + ), + prevProblemInfo!.prepared_main.code.name + ); + }} + > + + {prevProblemInfo!.prepared_main.code.name} + +
+
+ download icon +
+
+
+ + )} + { + setMainFile(file); + }} + /> + {!mainFile && ( + + )} +
+ ) : null} diff --git a/app/types/APIResponse.ts b/app/types/APIResponse.ts index e4bb475..6896e64 100644 --- a/app/types/APIResponse.ts +++ b/app/types/APIResponse.ts @@ -101,12 +101,16 @@ export interface SimpleProblemDetail { title: string; memory_limit: number; parsed_code_elements: codeHoles; + prepared_main: { + code: ServerSideFile; + language: lanugage; + }; testcases: { id: number; title: string; }[]; time_limit: number; - type: "solving" | "blank"; + type: "solving" | "blank" | "class_implementation"; start_time: string; end_time: string; gain_score: number; diff --git a/app/util/index.ts b/app/util/index.ts index 92f2256..3a1d9b5 100644 --- a/app/util/index.ts +++ b/app/util/index.ts @@ -1,4 +1,5 @@ import toast from "react-hot-toast"; +import { lanugage } from "~/types"; export function semesterToString(semester: number) { switch (semester) { @@ -118,3 +119,43 @@ export function bytesToSize(bytes: number) { const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`; } + +export const problemTitles: { [key: string]: string } = { + blank: "빈칸 채우기", + solution: "문제 해결", + class_implementation: "클래스 구현", +}; + +export function readFileAsServerFormat( + file: File +): Promise<{ content: string; name: string }> { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => + resolve({ + content: reader.result as string, + name: file.name, + }); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); +} + +export function getCodeFileExtension(language: lanugage) { + switch (language) { + case "c": + return "c"; + case "java": + return "java"; + case "python": + return "py"; + case "javascript": + return "js"; + case "cpp": + return "cpp"; + case "plaintext": + return "txt"; + default: + throw new Error(`Invalid language ${language}`); + } +}