Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const App = () => {
<I18nProvider translations={translations}>
<BrowserRouter>
<PageViewTracker />
<ToastContainer />
<ToastContainer position="top-center" />
<Routes>
<Route path="/" element={<MakeQuiz />} />
<Route path="/quiz/:problemSetId" element={<SolveQuiz />} />
Expand Down
42 changes: 38 additions & 4 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
36 changes: 22 additions & 14 deletions src/pages/MakeQuiz/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand Down
49 changes: 39 additions & 10 deletions src/pages/MakeQuiz/ui/RecentChanges/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="recent-changes-section">
<h3>{t("최근 변경사항")}</h3>
<ul className="changes-list">
<li>
<span className="date">2025.12.17</span>
<span className="change-text">{t("문제 가독성 개선")}</span>
</li>
<li>
<span className="date">2025.12.14</span>
<span className="change-text">
{t("페이지 제한 100pages → 150pages 변경")}
</span>
</li>
{changes.map((log, index) => (
<li key={log.id || index}>
<span className="date">{formatDate(log.dateTime)}</span>
<span className="change-text">{t(log.updateText)}</span>
</li>
))}
</ul>
</div>
);
Expand Down
43 changes: 43 additions & 0 deletions src/pages/MakeQuiz/util/fileUploader.js
Original file line number Diff line number Diff line change
@@ -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("변환 시간 초과");
}
2 changes: 1 addition & 1 deletion src/shared/toast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down