diff --git a/app/components/Modal/modal.module.css b/app/components/Modal/modal.module.css index 50e8471..02f4c9b 100644 --- a/app/components/Modal/modal.module.css +++ b/app/components/Modal/modal.module.css @@ -26,7 +26,6 @@ flex-direction: column; gap: 54px; position: relative; - width: 460px; border-radius: 24px; background: #fff; box-shadow: 0px 32px 64px 0px rgba(44, 39, 56, 0.04), diff --git a/app/components/common/form.module.css b/app/components/common/form.module.css index aab32f7..e2ad933 100644 --- a/app/components/common/form.module.css +++ b/app/components/common/form.module.css @@ -36,3 +36,16 @@ background-color: #0880ae; color: #ebf4f8; } + +.secondary-button { + border: none; + cursor: pointer; + display: flex; + width: 100%; + height: 45px; + justify-content: center; + align-items: center; + border-radius: 6px; + background-color: #756f86; + color: #ebf4f8; +} diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/BlankPreviewModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/_layout/BlankPreviewModal.tsx new file mode 100644 index 0000000..d2c8e52 --- /dev/null +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/BlankPreviewModal.tsx @@ -0,0 +1,41 @@ +import Modal from "~/components/Modal"; +import CodeBlank from "~/components/CodeBlank"; +import { codeHole, parsedCodeElement } from "~/util/codeHole"; +import toast from "react-hot-toast"; + +interface Props { + isOpen: boolean; + onClose: () => void; + codeString: string; + language: string; +} + +const BlankPreviewModal = ({ + isOpen, + onClose, + codeString, + language, +}: Props) => { + let parsedCode: parsedCodeElement[][]; + try { + parsedCode = codeHole(codeString, language); + } catch (error) { + toast.error( + "올바르게 빈칸을 렌더링 할 수 없습니다. 코드를 다시 확인해주세요" + ); + onClose(); + return null; + } + return ( + + + + ); +}; + +export default BlankPreviewModal; diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx index c176769..8d0b2eb 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemAddModal.tsx @@ -1,20 +1,188 @@ +import { useState } from "react"; import Modal from "~/components/Modal"; import styles from "./modal.module.css"; +import formStyles from "~/components/common/form.module.css"; +import RadioGroup from "~/components/Radio/RadioGroup"; +import TextInput from "~/components/Input/TextInput"; +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 { useAuth } from "~/contexts/AuthContext"; +import BlankPreviewModal from "./BlankPreviewModal"; interface Props { + lectureName: string; + practiceName: string; + practiceId: number; isOpen: boolean; onClose: () => void; } -const ProblemAddModal = ({ isOpen, onClose }: Props) => { +const ProblemAddModal = ({ + lectureName, + practiceName, + practiceId, + isOpen, + onClose, +}: Props) => { + const [problemType, setProblemType] = useState<"blank" | "solving">( + "solving" + ); + const [language, setLanguage] = useState< + "c" | "java" | "javascript" | "python" | "plaintext" + >("c"); + const [codeString, setCodeString] = useState(""); + const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); + const auth = useAuth(); return ( - + { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const name = formData.get("name") as string; + const time = parseInt(formData.get("time") as string, 10); + const memory = parseInt(formData.get("memory") as string, 10); + const file = formData.get("pdf") as File; + + switch (problemType) { + case "blank": + let holes: parsedCodeElement[][] = []; + try { + holes = codeHole(codeString, language); + } catch { + toast.error("블록 주석에 오류가 있습니다!"); + return; + } + const blankResponse = await postBlankProblem( + file, + memory, + holes, + practiceId, + time, + name, + auth.token + ); + if (blankResponse.status === 201) { + toast.success("문제를 성공적으로 추가했습니다!"); + onClose(); + } + break; + case "solving": + const solvingResponse = await postSolveProblem( + file, + memory, + practiceId, + time, + name, + auth.token + ); + if (solvingResponse.status === 201) { + toast.success("문제를 성공적으로 추가했습니다!"); + onClose(); + } + break; + } + }} + > + + + + void} + /> + + + { + return file.name.endsWith(".pdf"); + }} + onFileUpload={() => {}} + accept="application/pdf" + /> + + 문제 저장 + + + + {problemType === "blank" ? ( + + 빈칸 언어 + + setLanguage( + e.target.value as + | "c" + | "java" + | "javascript" + | "python" + | "plaintext" + ) + } + > + C + Java + JavaScript + Python + Text + + + setIsPreviewModalOpen(true)} + > + 빈칸 미리보기 + + {isPreviewModalOpen && ( + setIsPreviewModalOpen(false)} + codeString={codeString} + language={language} + /> + )} + + ) : null} + + + ); }; diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx index 9546548..3385bfd 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/ProblemEditModal.tsx @@ -1,12 +1,15 @@ import Modal from "~/components/Modal"; import styles from "./modal.module.css"; +import { useState } from "react"; interface Props { isOpen: boolean; onClose: () => void; + editingProblemId: number; } -const ProblemAddModal = ({ isOpen, onClose }: Props) => { +const ProblemEditModal = ({ isOpen, onClose, editingProblemId }: Props) => { + const [loading, setLoading] = useState(true); return ( { ); }; -export default ProblemAddModal; +export default ProblemEditModal; diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/TestCaseEditModal.tsx b/app/routes/_procted+/lectures+/$lectureId+/_layout/TestCaseEditModal.tsx index 7769553..b4d1b3d 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/_layout/TestCaseEditModal.tsx +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/TestCaseEditModal.tsx @@ -4,9 +4,10 @@ import styles from "./modal.module.css"; interface Props { isOpen: boolean; onClose: () => void; + testCaseId: number; } -const TestCaseEditModal = ({ isOpen, onClose }: Props) => { +const TestCaseEditModal = ({ isOpen, onClose, testCaseId }: Props) => { return ( { const [isLoading, setIsLoading] = useState(true); @@ -99,6 +104,7 @@ const LectureDetail = () => { /> {currentLecture!.practices.map((practice) => ( { export default LectureDetail; interface DetailProps { + lectureName: string; id: number; title: string; } -const PracticeDetail = ({ id, title }: DetailProps) => { +const PracticeDetail = ({ id, title, lectureName }: DetailProps) => { const auth = useAuth(); const navigate = useNavigate(); const [loading, setLoading] = useState(true); const [practiceDetail, setPracticeDetail] = useState(); const [isPracticeEditModalOpen, setIsPracticeEditModalOpen] = useState(false); - const [editingPracticeId, setEditingPracticeId] = useState(-1); + const [isProblemAddModalOpen, setIsProblemAddModalOpen] = useState(false); useEffect(() => { async function getData() { const response = await getPracticeWithPracticeId(id, auth.token); @@ -165,7 +172,6 @@ const PracticeDetail = ({ id, title }: DetailProps) => { title={title} isEditable={auth.role === "professor"} onEditClick={() => { - setEditingPracticeId(id); setIsPracticeEditModalOpen(true); }} onDeleteClick={async () => { @@ -179,19 +185,32 @@ const PracticeDetail = ({ id, title }: DetailProps) => { > {practiceDetail!.problems.map((problem) => ( ))} + setIsProblemAddModalOpen(true)} + iconSrcList={[plusSVG]} + /> {isPracticeEditModalOpen ? ( setIsPracticeEditModalOpen(false)} - practiceId={editingPracticeId} + practiceId={practiceDetail!.id} /> ) : null} + setIsProblemAddModalOpen(false)} + lectureName={lectureName} + practiceName={title} + practiceId={practiceDetail!.id} + /> > ); }; @@ -202,18 +221,18 @@ const ProblemDetail = ({ id, title }: DetailProps) => { const params = useParams(); const [loading, setLoading] = useState(true); const [problemDetail, setProblemDetail] = useState(); - const [isProblemAddModalOpen, setIsProblemAddModalOpen] = useState(false); const [isProblemEditModalOpen, setIsProblemEditModalOpen] = useState(false); - const [editingProblemId, setEditingProblemId] = useState(-1); - const [isTestCaseAddModalOpen, setTestCaseAddModalOpen] = useState(false); - const [isTestCaseEditModalOpen, setTestCaseEditModalOpen] = useState(false); + const [isTestCaseAddModalOpen, setIsTestCaseAddModalOpen] = useState(false); + const [isTestCaseEditModalOpen, setIsTestCaseEditModalOpen] = useState(false); const [editingTestCaseId, setEditingTestCaseId] = useState(-1); useEffect(() => { async function getData() { const response = await getProblemWithProblemId(id + "", auth.token); - setProblemDetail(response.data); - setLoading(false); + if (isSuccessResponse(response)) { + setProblemDetail((response as SuccessProblemDetailResponse).data); + setLoading(false); + } } getData(); }, []); @@ -228,7 +247,6 @@ const ProblemDetail = ({ id, title }: DetailProps) => { title={title} isEditable={auth.role === "professor"} onEditClick={() => { - setEditingProblemId(id); setIsProblemEditModalOpen(true); }} onDeleteClick={() => { @@ -247,7 +265,7 @@ const ProblemDetail = ({ id, title }: DetailProps) => { onIconClickList={[ () => { setEditingTestCaseId(testcase.id); - setTestCaseEditModalOpen(true); + setIsTestCaseEditModalOpen(true); }, () => { if ( @@ -261,17 +279,33 @@ const ProblemDetail = ({ id, title }: DetailProps) => { ]} onButtonClick={() => { setEditingTestCaseId(testcase.id); - setTestCaseEditModalOpen(true); + setIsTestCaseEditModalOpen(true); }} /> ))} { - setTestCaseAddModalOpen(true); + setIsTestCaseAddModalOpen(true); }} iconSrcList={[plusSVG]} /> + setIsProblemEditModalOpen(false)} + editingProblemId={id} + /> + setIsTestCaseAddModalOpen(false)} + /> + {isTestCaseEditModalOpen ? ( + setIsTestCaseEditModalOpen(false)} + testCaseId={editingTestCaseId} + /> + ) : null} ); }; diff --git a/app/routes/_procted+/lectures+/$lectureId+/_layout/modal.module.css b/app/routes/_procted+/lectures+/$lectureId+/_layout/modal.module.css index 8e578d2..7321471 100644 --- a/app/routes/_procted+/lectures+/$lectureId+/_layout/modal.module.css +++ b/app/routes/_procted+/lectures+/$lectureId+/_layout/modal.module.css @@ -1,6 +1,19 @@ +.modal-section { + display: flex; + gap: 50px; +} + .modal-body { display: flex; flex-direction: column; align-items: center; gap: 30px; } + +.blank-section { + display: flex; + flex-direction: column; + align-items: center; + min-width: 400px; + gap: 10px; +}