diff --git a/src/App.jsx b/src/App.jsx index 3cdff47..be0c3f4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -63,7 +63,7 @@ const App = () => { - + } /> } /> diff --git a/src/index.css b/src/index.css index 39664f0..7687e96 100644 --- a/src/index.css +++ b/src/index.css @@ -1,7 +1,41 @@ -div { - white-space: pre-line; +@font-face { + font-family: 'Pretendard'; + src: url('/fonts/Pretendard-Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; + font-display: swap; } -p { - white-space: pre-line; +@font-face { + font-family: 'Pretendard'; + src: url('/fonts/Pretendard-Medium.otf') format('opentype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('/fonts/Pretendard-Bold.otf') format('opentype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +/* 전역 폰트 적용 */ +* { + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; +} + +body { + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input, textarea, select, button { + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; } diff --git a/src/pages/MakeQuiz/index.jsx b/src/pages/MakeQuiz/index.jsx index 1deb6f2..7ab5001 100644 --- a/src/pages/MakeQuiz/index.jsx +++ b/src/pages/MakeQuiz/index.jsx @@ -12,19 +12,25 @@ import "react-pdf/dist/Page/TextLayer.css"; import { useNavigate } from "react-router-dom"; import "./index.css"; import { OcrButton, RecentChanges } from "./ui"; +import { uploadFileToServer } from "./util/fileUploader"; const levelDescriptions = { RECALL: `순수 암기나 단순 이해를 묻는 문제 - 예) "명제의 _______는 모든 가능한 경우에서 항상 참(True)이 되는 명제를 의미한다."`, + 예) "대한민국의 수도는 _______이다."`, SKILLS: `옳고 그름을 판별하는 문제 - 예) "명제 p → q의 대우(contrapositive)와 역(converse)이 모두 참일 때, 반드시 원래의 명제 p → q도 참이 된다." (O/X)`, + 예) "지구는 태양 주위를 돈다. (O/X)"`, STRATEGIC: `추론, 문제 해결, 자료 해석을 요구하는 문제 - 예) "교수님이 학생들에게 기말고사에서 100점을 받으면 A를 주겠다"라고 약속했습니다. 다음 중 이 논리적 함의(p → q)가 거짓(False)이 되는 경우는?"`, + 예) [전제] 물가가 오르면 화폐 가치는 떨어진다. 현재 물가가 급등했다. + [질문] 이 경우 화폐 가치의 변화로 가장 적절한 것은? + 1. 하락한다 + 2. 상승한다 + 3. 변함없다 + 4. 알 수 없다`, }; const MAX_FILE_SIZE = 30 * 1024 * 1024; @@ -87,15 +93,7 @@ const MakeQuiz = () => { [] ); - async function uploadFileToServer(file) { - const formData = new FormData(); - // 백엔드 @RequestPart("file") 과 동일한 키 - formData.append("file", file); - const res = await axiosInstance.post(`/s3/upload`, formData, { - isMultipart: true, - }); - return res.data; - } + // questionType 변경 시 quizLevel 자동으로 변경 및 localStorage에 저장 useEffect(() => { setQuizLevel(levelMapping[questionType]); @@ -178,7 +176,7 @@ const MakeQuiz = () => { setFileExtension(ext); setIsProcessing(true); try { - const { uploadedUrl } = await uploadFileToServer(f); + const uploadedUrl = await uploadFileToServer(f); setUploadedUrl(uploadedUrl); setFile(f); @@ -190,7 +188,17 @@ const MakeQuiz = () => { if (uploadTimerRef.current) { uploadTimerRef.current.stop(); } - throw error; + + const message = + error?.message === "변환 시간 초과" + ? t("파일 변환이 지연되고 있어요. 잠시 후 다시 시도해주세요.") + : error?.response?.data?.message || + error?.message || + t("파일 업로드 중 오류가 발생했습니다. 다시 시도해주세요."); + + CustomToast.error(message); + console.error("파일 업로드 실패:", error); + return; } finally { setFileExtension(null); setIsProcessing(false); diff --git a/src/pages/MakeQuiz/ui/RecentChanges/index.jsx b/src/pages/MakeQuiz/ui/RecentChanges/index.jsx index 6854983..2484389 100644 --- a/src/pages/MakeQuiz/ui/RecentChanges/index.jsx +++ b/src/pages/MakeQuiz/ui/RecentChanges/index.jsx @@ -1,22 +1,51 @@ +import { useEffect, useState } from "react"; import { useTranslation } from "i18nexus"; +import axiosInstance from "#shared/api"; import "./index.css"; const RecentChanges = () => { const { t } = useTranslation(); + const [changes, setChanges] = useState([]); + + useEffect(() => { + const fetchUpdates = async () => { + try { + const res = await axiosInstance.get("/updateLog"); + + const data = res.data; + + setChanges(data.updateLogs || []); + } catch (err) { + console.error("변경사항 로드 실패:", err); + } + }; + + fetchUpdates(); + }, []); + + const formatDate = (isoString) => { + const date = new Date(isoString); + return new Intl.DateTimeFormat("ko-KR", { + timeZone: "Asia/Seoul", + year: "numeric", + month: "2-digit", + day: "2-digit", + }) + .format(date) + .replace(/\. /g, ".") + .replace(/\.$/, ""); + }; + return (

{t("최근 변경사항")}

    -
  • - 2025.12.17 - {t("문제 가독성 개선")} -
  • -
  • - 2025.12.14 - - {t("페이지 제한 100pages → 150pages 변경")} - -
  • + {changes.map((log, index) => ( +
  • + {formatDate(log.dateTime)} + {t(log.updateText)} +
  • + ))}
); diff --git a/src/pages/MakeQuiz/util/fileUploader.js b/src/pages/MakeQuiz/util/fileUploader.js new file mode 100644 index 0000000..e12abeb --- /dev/null +++ b/src/pages/MakeQuiz/util/fileUploader.js @@ -0,0 +1,43 @@ +import axiosInstance from "#shared/api"; + +export async function uploadFileToServer(file) { + const initResponse = await axiosInstance.post("/s3/request-presign", { + originalFileName: file.name, + contentType: file.type, + fileSize: file.size, + }); + + const { uploadUrl, finalUrl, isPdf } = initResponse.data; + + const encodedFileName = encodeURIComponent(file.name); + + await axiosInstance.put(uploadUrl, file, { + headers: { + "Content-Type": file.type, + "x-amz-meta-original-filename": encodedFileName, + }, + }); + + if (!isPdf) { + await pollForFile(finalUrl); + } + + return finalUrl; +} + +// --------------------------------------------------------- +// Helper: DB 없이 파일 생성 여부 확인하기 (S3 직접 조회) +// --------------------------------------------------------- +async function pollForFile(url, timeout = 60000) { + const startTime = Date.now(); + + const encodedUrl = encodeURIComponent(url); + while (Date.now() - startTime < timeout) { + const res = await axiosInstance.get(`/s3/check-file-exist?url=${encodedUrl}`); + if (res.data.status === "EXIST") { + return true; + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + throw new Error("변환 시간 초과"); +} \ No newline at end of file diff --git a/src/shared/toast/index.js b/src/shared/toast/index.js index 34f74db..f094778 100644 --- a/src/shared/toast/index.js +++ b/src/shared/toast/index.js @@ -2,7 +2,7 @@ import { Slide, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; const options = { - position: "top-right", + position: "top-center", autoClose: 3000, hideProgressBar: true, closeOnClick: true,