Spaces:
Sleeping
Sleeping
| <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> | |