Kentlo's picture
쀑볡닡변 κ°€λŠ₯ 버전
08348fb
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>AZ-104 CBT μ—°μŠ΅</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
--brand: #0a6bdf;
--brand-dark: #0850a7;
--warn: #ffcc00;
--bg: #f5f7fa;
--ok: #178a00;
--bad: #d12929;
--card: #fff;
--muted: #6b7280;
--text: #1f2937;
}
body {
margin: 0;
background: var(--bg);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Apple SD Gothic Neo, Noto Sans KR, Arial, sans-serif;
padding: 24px;
display: flex;
justify-content: center;
color: var(--text);
}
.wrap { width: 100%; max-width: 900px; }
/* Topbar */
.topbar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px; gap: 12px; flex-wrap: wrap;
}
.progress { color: var(--muted); font-size: 14px; font-weight: 500; }
.jump-box { display: flex; align-items: center; gap: 8px; }
.jump-box input {
width: 60px; padding: 6px 10px; border: 1px solid #e5e7eb;
border-radius: 8px; font-size: 14px; text-align: center;
outline: none;
}
.jump-box input:focus { border-color: var(--brand); }
.jump-box button {
padding: 6px 12px; border: 1px solid #e5e7eb; border-radius: 8px;
background: #fff; cursor: pointer; font-size: 13px;
transition: all .15s;
}
.jump-box button:hover {
background: var(--brand); color: #fff; border-color: var(--brand);
}
.pill {
font-size: 12px; padding: 6px 12px; border-radius: 999px;
background: #e0e7ff; color: var(--brand-dark); font-weight: 600;
}
/* Card */
.card {
background: var(--card); border-radius: 16px; padding: 28px;
box-shadow: 0 10px 30px rgba(0,0,0,.06);
min-height: 300px; position: relative;
}
/* Fade Animation */
.fade-in { animation: fadeIn 0.3s ease-out; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.qid { font-size: 13px; color: var(--muted); margin-bottom: 8px; font-weight: 600; }
.qtext { font-size: 19px; line-height: 1.55; margin: 0 0 20px; font-weight: 700; word-break: keep-all; }
/* Options */
.options { display: grid; gap: 12px; margin-top: 16px; }
.opt {
display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px;
border: 2px solid #e5e7eb; border-radius: 12px; cursor: pointer; background: #fff;
transition: all .2s; position: relative;
}
.opt:hover:not(.disabled) { background: #f8fafc; border-color: #cbd5e1; }
.opt.selected { background: #eff6ff; border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
.opt.correct { border-color: var(--ok); background: #f0fdf4; }
.opt.wrong { border-color: var(--bad); background: #fef2f2; }
.opt.disabled { cursor: default; opacity: 0.8; pointer-events: none; }
/* Input Custom Style */
.opt input { margin-top: 4px; accent-color: var(--brand); cursor: pointer; }
.opt-key {
font-size: 12px; font-weight: 700; color: var(--muted);
min-width: 20px; margin-top: 3px;
}
/* Explanation */
.exp {
display: none; margin-top: 20px; padding: 16px;
border-left: 5px solid var(--brand);
background: #f0f9ff; border-radius: 8px;
line-height: 1.6;
}
.exp.show { display: block; animation: fadeIn 0.3s; }
.exp .title { margin: 0 0 8px; font-weight: 700; font-size: 16px; display: flex; align-items: center; gap: 6px; }
.exp .ok { color: var(--ok); }
.exp .bad { color: var(--bad); }
/* Navigation */
.nav {
margin-top: 20px; display: flex; align-items: center; gap: 12px;
background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,.06);
padding: 16px 20px; flex-wrap: wrap;
}
.nav-group { display: flex; gap: 8px; }
.spacer { flex: 1; }
button.btn {
border: 0; padding: 12px 20px; border-radius: 10px; color: #fff; background: var(--brand);
cursor: pointer; font-weight: 600; font-size: 15px;
transition: background .15s, transform .05s;
}
button.btn:active { transform: scale(0.98); }
button.btn:hover:not([disabled]) { background: var(--brand-dark); }
button.outline {
background: #fff; color: #374151; border: 1px solid #d1d5db;
}
button.outline:hover { background: #f9fafb; border-color: #9ca3af; }
button.bookmark { background: #fffbeb; color: #92400e; border: 1px solid #fcd34d; }
button.bookmark:hover { background: #fef3c7; }
button[disabled] { opacity: .5; cursor: not-allowed; background: #9ca3af; }
.loading { text-align: center; padding: 60px 0; color: var(--muted); font-size: 15px; }
/* Toast Notification */
#toast-container {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
z-index: 1000; display: flex; flex-direction: column; gap: 10px; pointer-events: none;
}
.toast {
background: #1f2937; color: #fff; padding: 12px 24px; border-radius: 50px;
font-size: 14px; opacity: 0; transform: translateY(20px);
transition: all 0.3s; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.toast.show { opacity: 1; transform: translateY(0); }
/* Multi-answer hint */
.multi-hint {
margin-top: 10px;
font-size: 13px;
color: var(--muted);
display: none;
}
.multi-hint.show { display: block; }
/* Mobile Responsive */
@media (max-width: 640px) {
body { padding: 12px; padding-bottom: 80px; }
.topbar { flex-direction: column; align-items: stretch; gap: 10px; }
.jump-box { justify-content: space-between; }
.jump-box input { width: 100%; flex:1; }
.card { padding: 20px; }
.qtext { font-size: 17px; }
.nav {
flex-direction: column; gap: 10px; padding: 12px 16px;
position: fixed; bottom: 0; left: 0; right: 0;
border-radius: 20px 20px 0 0; box-shadow: 0 -4px 20px rgba(0,0,0,0.08);
z-index: 50;
}
.nav-group { width: 100%; display: grid; grid-template-columns: 1fr 1fr; }
button.btn { width: 100%; padding: 12px; }
.spacer { display: none; }
#homeBtn, #bmBtn { display: none; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<div class="pill">AZ-104 CBT μ—°μŠ΅</div>
<div class="spacer" style="flex:1"></div>
<div class="progress" id="progress">문제 - / -</div>
<div class="jump-box">
<input type="number" id="jumpInput" placeholder="No." min="1" />
<button id="jumpBtn">이동</button>
</div>
</div>
<div class="card">
<div id="loading" class="loading">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="animation: spin 1s linear infinite; margin-bottom: 10px; display:block; margin: 0 auto 10px;">
<path d="M12 2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M12 18V22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4.93 4.93L7.76 7.76" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16.24 16.24L19.07 19.07" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M2 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M18 12H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4.93 19.07L7.76 16.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16.24 7.76L19.07 4.93" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<style>@keyframes spin { 100% { transform: rotate(360deg); } }</style>
문제λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...
</div>
<div id="qContainer" style="display: none;">
<div class="qid" id="qid"></div>
<h2 class="qtext" id="qtext"></h2>
<!-- βœ… λ©€ν‹°/μŠ€ν… 힌트 -->
<div id="multiHint" class="multi-hint"></div>
<div class="options" id="options"></div>
<div class="exp" id="exp"></div>
</div>
</div>
<div class="nav">
<button class="btn outline" id="homeBtn" title="ν™ˆμœΌλ‘œ">🏠</button>
<div class="nav-group">
<button class="btn outline" id="prevBtn">← 이전</button>
<button class="btn outline" id="skipBtn">μŠ€ν‚΅ β†’</button>
</div>
<div class="spacer"></div>
<button class="btn" id="submitBtn" disabled>μ •λ‹΅ 확인 (Enter)</button>
<button class="btn" id="nextBtn" style="display: none;">λ‹€μŒ 문제 β†’</button>
<button class="btn bookmark" id="bmBtn">⭐ 볡슡 μ €μž₯</button>
</div>
</div>
<div id="toast-container"></div>
<script>
/* --- Global State --- */
let currentQuestion = null;
let answered = false;
// βœ… 단일/볡수/μŠ€ν…μ— 따라 UI/채점 λͺ¨λ“œ
// mode: "single" | "multi" | "steps"
let answerMode = "single";
// βœ… 선택값
// single: string("A")
// multi/steps: array(["A","C"]) / array(["E","B","C"])
let selected = null;
/* --- Elements --- */
const els = {
qid: document.getElementById("qid"),
qtext: document.getElementById("qtext"),
opts: document.getElementById("options"),
exp: document.getElementById("exp"),
progress: document.getElementById("progress"),
submitBtn: document.getElementById("submitBtn"),
nextBtn: document.getElementById("nextBtn"),
skipBtn: document.getElementById("skipBtn"),
prevBtn: document.getElementById("prevBtn"),
homeBtn: document.getElementById("homeBtn"),
bmBtn: document.getElementById("bmBtn"),
jumpInput: document.getElementById("jumpInput"),
jumpBtn: document.getElementById("jumpBtn"),
loading: document.getElementById("loading"),
qContainer: document.getElementById("qContainer"),
toastContainer: document.getElementById("toast-container"),
multiHint: document.getElementById("multiHint"),
};
/* --- Utils --- */
function showToast(message) {
const toast = document.createElement("div");
toast.className = "toast";
toast.textContent = message;
els.toastContainer.appendChild(toast);
void toast.offsetWidth;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), 300);
}, 2500);
}
function getLastQuestionId() {
return parseInt(localStorage.getItem("lastQuestionId")) || null;
}
function saveLastQuestionId(id) {
localStorage.setItem("lastQuestionId", id);
}
function setLoading(isLoading) {
els.loading.style.display = isLoading ? "block" : "none";
els.qContainer.style.display = isLoading ? "none" : "block";
}
function toKey(opt) {
// opt: "A. text" / "2-D. text" ν˜•νƒœ
const s = String(opt || "").trim();
if (!s) return "";
// 첫 토큰을 key둜: "A." / "2-D." / "B)" λ“±
const first = s.split(/\s+/)[0];
return first.replace(/[.)]$/, "").trim();
}
function inferModeFromQuestion(q) {
// βœ… Steps μš°μ„  (λ°±μ—”λ“œκ°€ answer_steps λ‚΄λ €μ£Όλ©΄ ν™•μ •)
if (Array.isArray(q.answer_steps) && q.answer_steps.length) return "steps";
// βœ… λ©€ν‹°: answer_keysκ°€ 2개 μ΄μƒμ΄κ±°λ‚˜ answer λ¬Έμžμ—΄μ— μ‰Όν‘œκ°€ 있으면
if (Array.isArray(q.answer_keys) && q.answer_keys.length >= 2) return "multi";
if (typeof q.answer === "string" && q.answer.includes(",")) return "multi";
return "single";
}
function setHint(mode) {
els.multiHint.classList.remove("show");
els.multiHint.textContent = "";
if (mode === "multi") {
els.multiHint.textContent = "πŸ’‘ 볡수 μ •λ‹΅ λ¬Έμ œμž…λ‹ˆλ‹€. 정닡을 μ—¬λŸ¬ 개 μ„ νƒν•œ λ’€ μ œμΆœν•˜μ„Έμš”.";
els.multiHint.classList.add("show");
} else if (mode === "steps") {
els.multiHint.textContent = "πŸ’‘ μˆœμ„œ(단계) λ¬Έμ œμž…λ‹ˆλ‹€. 정닡을 μˆœμ„œλŒ€λ‘œ ν΄λ¦­ν•˜μ„Έμš”. (λ‹€μ‹œ ν΄λ¦­ν•˜λ©΄ λ§ˆμ§€λ§‰ 선택이 μ·¨μ†Œλ©λ‹ˆλ‹€)";
els.multiHint.classList.add("show");
}
}
function resetState() {
selected = null;
answered = false;
els.submitBtn.disabled = true;
els.submitBtn.style.display = "block";
els.nextBtn.style.display = "none";
els.skipBtn.style.display = "block";
els.exp.classList.remove("show");
els.exp.innerHTML = "";
els.opts.innerHTML = "";
}
function updateSubmitEnabled() {
if (answerMode === "single") {
els.submitBtn.disabled = !selected;
} else {
els.submitBtn.disabled = !(Array.isArray(selected) && selected.length > 0);
}
}
// steps λͺ¨λ“œ: 선택 μˆœμ„œ ν‘œμ‹œ(μ˜΅μ…˜ 우츑 상단 λ°°μ§€ λŒ€μ‹ , κ°„λ‹¨νžˆ selected 클래슀둜만 μœ μ§€)
function toggleSelectedClass(labelEl, on) {
if (on) labelEl.classList.add("selected");
else labelEl.classList.remove("selected");
}
function clearAllSelectedClass() {
document.querySelectorAll(".opt").forEach(el => el.classList.remove("selected"));
}
function getSelectedArray() {
if (!Array.isArray(selected)) return [];
return selected.slice();
}
/* --- Core Logic --- */
async function loadQuestion(questionId = null) {
setLoading(true);
try {
const url = questionId ? `/api/question?id=${questionId}` : "/api/question";
const res = await fetch(url);
if (!res.ok) throw new Error("λ„€νŠΈμ›Œν¬ 응닡 였λ₯˜");
const data = await res.json();
if (data.error) {
els.loading.innerHTML = `<span style="color:var(--bad)">⚠️ ${data.error}</span>`;
return;
}
currentQuestion = data;
render(data);
saveLastQuestionId(data.id);
setLoading(false);
} catch (err) {
els.loading.innerHTML = `<span style="color:var(--bad)">⚠️ 문제λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.<br><small>${err.message}</small></span>`;
console.error(err);
}
}
function render(q) {
els.qContainer.classList.remove("fade-in");
void els.qContainer.offsetWidth;
els.qContainer.classList.add("fade-in");
els.progress.textContent = `문제 ${q.id} / ${q.total}`;
els.qid.textContent = `Question ID: ${q.id}`;
els.qtext.textContent = q.question;
els.jumpInput.max = q.total;
els.jumpInput.placeholder = `Total ${q.total}`;
resetState();
// βœ… λͺ¨λ“œ νŒλ³„ + 힌트
answerMode = inferModeFromQuestion(q);
setHint(answerMode);
// βœ… μ˜΅μ…˜ λ Œλ”
(q.options || []).forEach((opt, index) => {
const line = (typeof opt === "string") ? opt : `${opt.key}. ${opt.text}`;
const key = toKey(line);
let text = line.replace(/^[^\s]+[\s]*/, ""); // 첫 토큰 제거 ν›„ λ‚˜λ¨Έμ§€
// "A. xxx" 같은 경우 μ•žμ— "A." μ œκ±°κ°€ 덜 될 수 μžˆμ–΄ 보정
text = text.replace(/^[A-Z]\s*[\.\)]\s*/, "");
const label = document.createElement("label");
label.className = "opt";
label.dataset.key = key;
label.dataset.idx = index + 1;
const input = document.createElement("input");
input.name = `q${q.id}`;
// βœ… single=radio, multi/steps=checkbox (steps도 μ²΄ν¬λ°•μŠ€λ₯Ό μ“°λ˜, 클릭 μˆœμ„œλ‘œ λ°°μ—΄ 관리)
input.type = (answerMode === "single") ? "radio" : "checkbox";
input.value = key;
// 클릭/λ³€κ²½ 이벀트 톡합 처리
input.addEventListener("change", (e) => onSelect(key, label, e.target.checked));
const keySpan = document.createElement("div");
keySpan.className = "opt-key";
keySpan.textContent = key + ".";
const textSpan = document.createElement("div");
textSpan.style.flex = "1";
textSpan.textContent = text;
label.appendChild(input);
label.appendChild(keySpan);
label.appendChild(textSpan);
els.opts.appendChild(label);
// label 클릭해도 ν† κΈ€λ˜κ²Œ(λͺ¨λ°”일 체감 μ’‹μŒ)
label.addEventListener("click", (e) => {
if (answered) return;
// input 자체 클릭은 κΈ°λ³Έ λ™μž‘μ— λ§‘κΈ°κ³ , label μ˜μ—­ 클릭만 보정
if (e.target.tagName !== "INPUT") input.click();
});
});
}
function onSelect(key, labelEl, checked) {
if (answered) return;
if (answerMode === "single") {
// κΈ°μ‘΄ λ™μž‘ μœ μ§€
document.querySelectorAll(".opt").forEach(opt => opt.classList.remove("selected"));
labelEl.classList.add("selected");
selected = key;
updateSubmitEnabled();
return;
}
// multi / steps: λ°°μ—΄λ‘œ 관리
if (!Array.isArray(selected)) selected = [];
if (answerMode === "multi") {
// checkbox κ·ΈλŒ€λ‘œ 반영
if (checked) {
if (!selected.includes(key)) selected.push(key);
toggleSelectedClass(labelEl, true);
} else {
selected = selected.filter(x => x !== key);
toggleSelectedClass(labelEl, false);
}
updateSubmitEnabled();
return;
}
// steps: 클릭 μˆœμ„œκ°€ μ€‘μš”
// κ·œμΉ™: 체크되면 push, ν•΄μ œλ˜λ©΄ "λ§ˆμ§€λ§‰ μš”μ†Œ"일 λ•Œλ§Œ pop ν—ˆμš©(μ‚¬μš©μž μ‹€μˆ˜ λ°©μ§€)
if (checked) {
if (!selected.includes(key)) selected.push(key);
toggleSelectedClass(labelEl, true);
} else {
// λ§ˆμ§€λ§‰λ§Œ μ·¨μ†Œ ν—ˆμš©
const last = selected[selected.length - 1];
if (last === key) {
selected.pop();
toggleSelectedClass(labelEl, false);
} else {
// 되돌리고 μ•ˆλ‚΄
const input = labelEl.querySelector("input");
if (input) input.checked = true;
showToast("μˆœμ„œ λ¬Έμ œλŠ” λ§ˆμ§€λ§‰ μ„ νƒλ§Œ μ·¨μ†Œν•  수 μžˆμ–΄μš”.");
}
}
updateSubmitEnabled();
}
// μ •λ‹΅ 제좜
els.submitBtn.addEventListener("click", async () => {
if (answered) return;
// 선택 κ°’ λ§Œλ“€κΈ°
let chosenPayload = null;
if (answerMode === "single") {
if (!selected) return;
chosenPayload = selected;
} else {
const arr = getSelectedArray();
if (!arr.length) return;
chosenPayload = arr.length === 1 ? arr[0] : arr; // λ°±μ—”λ“œ ν˜Έν™˜(1개면 string)
}
els.submitBtn.disabled = true;
try {
const res = await fetch("/api/answer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
question_id: currentQuestion.id,
chosen: chosenPayload,
user_id: "default"
})
});
const result = await res.json();
answered = true;
// μ •λ‹΅ν‚€/stepsν‚€λ₯Ό 받을 수 있으면 μš°μ„  μ‚¬μš©
// - μ΅œμ ν™” app.py: answer_keys / answer_stepsλ₯Ό 내렀쀄 수 있음
const answerKeys = Array.isArray(currentQuestion.answer_keys) ? currentQuestion.answer_keys.map(String) : [];
const answerSteps = Array.isArray(currentQuestion.answer_steps) ? currentQuestion.answer_steps.map(String) : [];
// μ„œλ²„κ°€ answerλ₯Ό "A,B"둜 μ£ΌλŠ” ꡬ버전이면 νŒŒμ‹±
const legacyKeys = (typeof result.answer === "string" && result.answer.includes(","))
? result.answer.split(",").map(x => x.trim()).filter(Boolean)
: [];
// μ±„μ μš© μ •λ‹΅ 리슀트 κ²°μ •
let correctList = [];
if (answerMode === "steps" && answerSteps.length) correctList = answerSteps;
else if (answerMode !== "single" && answerKeys.length) correctList = answerKeys;
else if (legacyKeys.length) correctList = legacyKeys;
else if (typeof result.answer === "string" && result.answer) correctList = [result.answer];
// μŠ€νƒ€μΌ 적용
document.querySelectorAll(".opt").forEach(optEl => {
optEl.classList.add("disabled");
const optKey = optEl.dataset.key;
// correctList에 있으면 correct ν‘œκΈ° (λ©€ν‹°λ©΄ μ—¬λŸ¬κ°œ)
if (correctList.includes(optKey)) {
optEl.classList.add("correct");
}
// μ˜€λ‹΅ ν‘œκΈ°: μ„ νƒν–ˆλŠ”λ° correctList에 μ—†μœΌλ©΄ wrong
if (answerMode === "single") {
if (optKey === selected && !result.correct && !correctList.includes(optKey)) {
optEl.classList.add("wrong");
}
} else {
const selArr = Array.isArray(selected) ? selected : [];
if (selArr.includes(optKey) && !result.correct && !correctList.includes(optKey)) {
optEl.classList.add("wrong");
}
}
});
showExplanation(result, correctList);
els.submitBtn.style.display = "none";
els.nextBtn.style.display = "block";
els.skipBtn.style.display = "none";
els.nextBtn.focus();
} catch (err) {
console.error(err);
showToast("❌ 채점 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.");
els.submitBtn.disabled = false;
}
});
function showExplanation(result, correctList) {
els.exp.classList.add("show");
const isCorrect = !!result.correct;
const correctText = correctList.length ? correctList.join(", ") : (result.answer || "(μ •λ‹΅ 정보 μ—†μŒ)");
const extra =
(answerMode === "multi") ? `<div style="margin-top:6px;color:var(--muted);font-size:13px">선택: ${Array.isArray(selected) ? selected.join(", ") : selected}</div>` :
(answerMode === "steps") ? `<div style="margin-top:6px;color:var(--muted);font-size:13px">선택(μˆœμ„œ): ${Array.isArray(selected) ? selected.join(" β†’ ") : selected}</div>` :
"";
els.exp.innerHTML = `
<div class="title ${isCorrect ? 'ok':'bad'}">
${isCorrect ? "βœ… μ •λ‹΅μž…λ‹ˆλ‹€!" : "❌ μ˜€λ‹΅! 정닡은 " + correctText + " μž…λ‹ˆλ‹€."}
</div>
${extra}
<div>${result.explanation || "ν•΄μ„€ 정보가 μ—†μŠ΅λ‹ˆλ‹€."}</div>
`;
}
// λ‹€μŒ/이전/μŠ€ν‚΅
async function moveQuestion(direction) {
if (!currentQuestion) return;
if (direction === "prev") {
if (currentQuestion.id <= 1) { showToast("첫 번째 λ¬Έμ œμž…λ‹ˆλ‹€."); return; }
await loadQuestion(currentQuestion.id - 1);
return;
}
try {
const res = await fetch(`/api/next?current_id=${currentQuestion.id}`);
const data = await res.json();
if (data.end) { showToast("πŸŽ‰ λ§ˆμ§€λ§‰ λ¬Έμ œμž…λ‹ˆλ‹€!"); return; }
currentQuestion = data;
render(data);
saveLastQuestionId(data.id);
} catch (err) {
showToast("❌ 이동 μ‹€νŒ¨");
}
}
els.nextBtn.addEventListener("click", () => moveQuestion("next"));
els.skipBtn.addEventListener("click", () => moveQuestion("next"));
els.prevBtn.addEventListener("click", () => moveQuestion("prev"));
// 뢁마크
els.bmBtn.addEventListener("click", async () => {
if (!currentQuestion) return;
try {
const res = await fetch("/api/review_add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question_id: currentQuestion.id, user_id: "default" })
});
const data = await res.json();
showToast("⭐ " + (data.message || "볡슡 λ…ΈνŠΈμ— μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€."));
} catch (err) {
showToast("❌ μ €μž₯ μ‹€νŒ¨");
}
});
// 점프
function jumpTo() {
const targetId = parseInt(els.jumpInput.value);
if (!targetId || targetId < 1) { showToast("⚠️ μ˜¬λ°”λ₯Έ 번호λ₯Ό μž…λ ₯ν•˜μ„Έμš”."); return; }
if (currentQuestion && targetId > currentQuestion.total) { showToast(`⚠️ 1 ~ ${currentQuestion.total} μ‚¬μ΄λ§Œ κ°€λŠ₯ν•©λ‹ˆλ‹€.`); return; }
loadQuestion(targetId);
els.jumpInput.value = "";
}
els.jumpBtn.addEventListener("click", jumpTo);
els.jumpInput.addEventListener("keypress", (e) => { if (e.key === "Enter") jumpTo(); });
els.homeBtn.addEventListener("click", () => window.location.href = "/");
// ν‚€λ³΄λ“œ 단좕킀
window.addEventListener("keydown", e => {
if (document.activeElement.tagName === "INPUT") return;
const key = e.key.toUpperCase();
// 선택지 선택(1~4 / A~D) β€” λ©€ν‹°/μŠ€ν…μ—μ„œλ„ "ν† κΈ€"둜 λ™μž‘
if (!answered) {
const keyMap = { "1":"A", "2":"B", "3":"C", "4":"D", "A":"A", "B":"B", "C":"C", "D":"D" };
if (keyMap[key]) {
const targetKey = keyMap[key];
const targetLabel = document.querySelector(`.opt[data-key="${targetKey}"]`);
if (targetLabel) {
const input = targetLabel.querySelector("input");
if (input) input.click();
}
}
}
// 제좜/λ‹€μŒ (Enter)
if (e.key === "Enter") {
if (!els.submitBtn.disabled && els.submitBtn.style.display !== "none") {
els.submitBtn.click();
} else if (els.nextBtn.style.display !== "none") {
els.nextBtn.click();
}
}
// 이동 (쒌우)
if (e.key === "ArrowRight") {
if (els.nextBtn.style.display !== "none") els.nextBtn.click();
}
if (e.key === "ArrowLeft") {
els.prevBtn.click();
}
});
// 초기 μ‹€ν–‰
const lastId = getLastQuestionId();
loadQuestion(lastId || undefined);
</script>
</body>
</html>