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; | |
| } | |
| 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; | |
| } | |
| .wrap { width: 100%; max-width: 900px; } | |
| .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; } | |
| .jump-box { | |
| display: flex; align-items: center; gap: 8px; | |
| } | |
| .jump-box input { | |
| width: 70px; padding: 6px 10px; border: 1px solid #e5e7eb; | |
| border-radius: 8px; font-size: 14px; text-align: center; | |
| } | |
| .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 10px; border-radius: 999px; | |
| background: #f3f4f6; color: #374151; | |
| } | |
| .card { | |
| background: var(--card); border-radius: 14px; padding: 22px; | |
| box-shadow: 0 8px 24px rgba(0,0,0,.08); | |
| min-height: 260px; | |
| } | |
| .qid { font-size: 13px; color: var(--muted); margin-bottom: 6px; } | |
| .qtext { font-size: 18px; line-height: 1.5; margin: 0 0 14px; } | |
| .options { display: grid; gap: 10px; margin-top: 14px; } | |
| .opt { | |
| display: flex; align-items: center; gap: 10px; padding: 12px 14px; | |
| border: 1px solid #e5e7eb; border-radius: 10px; cursor: pointer; background: #fff; | |
| transition: background .15s, border-color .15s; | |
| } | |
| .opt:hover { background: #f0f6ff; border-color: #d1e3ff; } | |
| .opt.selected { background: #f0f6ff; border-color: var(--brand); } | |
| .opt.correct { border-color: #b8e3c1; background: #effaf2; } | |
| .opt.wrong { border-color: #f1b7b7; background: #fff1f1; } | |
| .opt.disabled { cursor: not-allowed; opacity: 0.7; } | |
| .opt input { accent-color: var(--brand); } | |
| .exp { | |
| display: none; margin-top: 14px; padding: 12px 14px; border-left: 5px solid var(--brand); | |
| background: #eef6ff; border-radius: 8px; | |
| } | |
| .exp.show { display: block; } | |
| .exp .title { margin: 0 0 6px; font-weight: 700; } | |
| .exp .ok { color: var(--ok); } | |
| .exp .bad { color: var(--bad); } | |
| .nav { | |
| margin-top: 18px; display: flex; align-items: center; gap: 14px; | |
| background: #fff; border-radius: 14px; box-shadow: 0 8px 24px rgba(0,0,0,.08); | |
| padding: 16px 18px; | |
| } | |
| .nav-group { | |
| display: flex; gap: 8px; | |
| } | |
| .spacer { flex: 1; } | |
| button.btn { | |
| border: 0; padding: 10px 18px; border-radius: 10px; color: #fff; background: var(--brand); | |
| cursor: pointer; font-weight: 600; | |
| transition: background .15s; | |
| } | |
| button.btn:hover { background: var(--brand-dark); } | |
| button.outline { | |
| background: #fff; color: #111827; border: 1px solid #e5e7eb; | |
| } | |
| button.bookmark { background: var(--warn); color: #1f2937; } | |
| button[disabled] { opacity: .5; cursor: not-allowed; } | |
| .loading { | |
| text-align: center; | |
| padding: 40px; | |
| color: var(--muted); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <div class="topbar"> | |
| <div class="pill">CBT โข ๋จ์ผ๋ฌธ์ ๋ชจ๋</div> | |
| <div class="jump-box"> | |
| <span style="font-size: 13px; color: var(--muted);">๋ฌธ์ ์ด๋:</span> | |
| <input type="number" id="jumpInput" placeholder="๋ฒํธ" min="1" /> | |
| <button id="jumpBtn">์ด๋</button> | |
| </div> | |
| <div class="progress" id="progress">๋ฌธ์ 0 / 0</div> | |
| </div> | |
| <div class="card"> | |
| <div id="loading" class="loading">๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...</div> | |
| <div id="qContainer" style="display: none;"> | |
| <div class="qid" id="qid"></div> | |
| <h2 class="qtext" id="qtext"></h2> | |
| <div class="options" id="options"></div> | |
| <div class="exp" id="exp"></div> | |
| </div> | |
| </div> | |
| <div class="nav"> | |
| <button class="btn outline" id="homeBtn">๐ ํ</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>์ ๋ต ํ์ธ</button> | |
| <button class="btn" id="nextBtn" style="display: none;">์ ๋ต ํ ๋ค์ โ</button> | |
| <button class="btn bookmark" id="bmBtn">โญ ๋ณต์ต</button> | |
| </div> | |
| </div> | |
| <script> | |
| let currentQuestion = null; | |
| let selectedOption = null; | |
| let answered = false; | |
| const qidEl = document.getElementById("qid"); | |
| const qtextEl = document.getElementById("qtext"); | |
| const optsEl = document.getElementById("options"); | |
| const expEl = document.getElementById("exp"); | |
| const progressEl = document.getElementById("progress"); | |
| const submitBtn = document.getElementById("submitBtn"); | |
| const nextBtn = document.getElementById("nextBtn"); | |
| const skipBtn = document.getElementById("skipBtn"); | |
| const prevBtn = document.getElementById("prevBtn"); | |
| const homeBtn = document.getElementById("homeBtn"); | |
| const bmBtn = document.getElementById("bmBtn"); | |
| const jumpInput = document.getElementById("jumpInput"); | |
| const jumpBtn = document.getElementById("jumpBtn"); | |
| const loadingEl = document.getElementById("loading"); | |
| const qContainer = document.getElementById("qContainer"); | |
| // localStorage์์ ๋ง์ง๋ง ๋ฌธ์ ๋ฒํธ ๊ฐ์ ธ์ค๊ธฐ | |
| function getLastQuestionId() { | |
| return parseInt(localStorage.getItem("lastQuestionId")) || null; | |
| } | |
| // localStorage์ ํ์ฌ ๋ฌธ์ ๋ฒํธ ์ ์ฅ | |
| function saveLastQuestionId(id) { | |
| localStorage.setItem("lastQuestionId", id); | |
| } | |
| // ๋ฌธ์ ๋ถ๋ฌ์ค๊ธฐ | |
| async function loadQuestion(questionId = null) { | |
| loadingEl.style.display = "block"; | |
| qContainer.style.display = "none"; | |
| try { | |
| const url = questionId ? `/api/question?id=${questionId}` : '/api/question'; | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| if (data.error) { | |
| loadingEl.textContent = "โ " + data.error; | |
| return; | |
| } | |
| currentQuestion = data; | |
| render(data); | |
| saveLastQuestionId(data.id); // ๋ฌธ์ ๋ฒํธ ์ ์ฅ | |
| loadingEl.style.display = "none"; | |
| qContainer.style.display = "block"; | |
| } catch (err) { | |
| loadingEl.textContent = "โ ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค."; | |
| console.error(err); | |
| } | |
| } | |
| // ๋ฌธ์ ๋ ๋๋ง | |
| function render(q) { | |
| progressEl.textContent = `๋ฌธ์ ${q.id} / ${q.total}`; | |
| qidEl.textContent = `ID ${q.id}`; | |
| qtextEl.textContent = q.question; | |
| optsEl.innerHTML = ""; | |
| expEl.classList.remove("show"); | |
| expEl.innerHTML = ""; | |
| // ์ ๋ ฅ์ฐฝ ์ต๋๊ฐ ์ค์ | |
| jumpInput.max = q.total; | |
| jumpInput.placeholder = `1-${q.total}`; | |
| // ์ด๊ธฐํ | |
| selectedOption = null; | |
| answered = false; | |
| submitBtn.disabled = true; | |
| submitBtn.style.display = "block"; | |
| nextBtn.style.display = "none"; | |
| skipBtn.style.display = "block"; | |
| (q.options || []).forEach(opt => { | |
| const label = document.createElement("label"); | |
| label.className = "opt"; | |
| const input = document.createElement("input"); | |
| input.type = "radio"; | |
| input.name = `q${q.id}`; | |
| input.value = opt; | |
| input.addEventListener("change", () => onSelect(opt, label)); | |
| label.appendChild(input); | |
| label.append(opt); | |
| optsEl.appendChild(label); | |
| }); | |
| } | |
| // ์ ํ | |
| function onSelect(chosen, labelEl) { | |
| if (answered) return; | |
| // ๋ชจ๋ ์ ํ ํด์ | |
| document.querySelectorAll('.opt').forEach(opt => { | |
| opt.classList.remove("selected"); | |
| }); | |
| // ์ ์ ํ | |
| labelEl.classList.add("selected"); | |
| selectedOption = chosen.charAt(0); // "A. Berlin" -> "A" | |
| submitBtn.disabled = false; | |
| } | |
| // ์ ๋ต ํ์ธ | |
| submitBtn.addEventListener("click", async () => { | |
| if (!selectedOption || answered) return; | |
| try { | |
| const res = await fetch("/api/answer", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| question_id: currentQuestion.id, | |
| chosen: selectedOption, | |
| user_id: "default" | |
| }) | |
| }); | |
| const result = await res.json(); | |
| answered = true; | |
| // ์ ๋ต/์ค๋ต ํ์ | |
| document.querySelectorAll('.opt').forEach(opt => { | |
| opt.classList.add("disabled"); | |
| const optLetter = opt.textContent.charAt(0); | |
| if (optLetter === result.answer) { | |
| opt.classList.add("correct"); | |
| } else if (optLetter === selectedOption && !result.correct) { | |
| opt.classList.add("wrong"); | |
| } | |
| }); | |
| // ํด์ค ํ์ | |
| showExplanation(result); | |
| submitBtn.style.display = "none"; | |
| nextBtn.style.display = "block"; | |
| skipBtn.style.display = "none"; | |
| } catch (err) { | |
| console.error(err); | |
| alert("์ฑ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."); | |
| } | |
| }); | |
| // ํด์ค ํ์ | |
| function showExplanation(result) { | |
| expEl.classList.add("show"); | |
| const isCorrect = result.correct; | |
| expEl.innerHTML = ` | |
| <div class="title ${isCorrect ? 'ok':'bad'}"> | |
| ${isCorrect ? "โ ์ ๋ต์ ๋๋ค!" : "โ ์ค๋ต์ ๋๋ค. ์ ๋ต: " + result.answer} | |
| </div> | |
| <div>${result.explanation || "ํด์ค์ด ์์ต๋๋ค."}</div> | |
| `; | |
| } | |
| // ๋ค์ ๋ฌธ์ | |
| nextBtn.addEventListener("click", async () => { | |
| try { | |
| const res = await fetch(`/api/next?current_id=${currentQuestion.id}`); | |
| const data = await res.json(); | |
| if (data.end) { | |
| alert("๋ง์ง๋ง ๋ฌธ์ ์ ๋๋ค!"); | |
| return; | |
| } | |
| currentQuestion = data; | |
| render(data); | |
| saveLastQuestionId(data.id); // ๋ฌธ์ ๋ฒํธ ์ ์ฅ | |
| } catch (err) { | |
| console.error(err); | |
| alert("๋ค์ ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค."); | |
| } | |
| }); | |
| // ์ด์ ๋ฌธ์ | |
| prevBtn.addEventListener("click", async () => { | |
| if (!currentQuestion || currentQuestion.id <= 1) { | |
| alert("์ฒซ ๋ฒ์งธ ๋ฌธ์ ์ ๋๋ค!"); | |
| return; | |
| } | |
| try { | |
| const prevId = currentQuestion.id - 1; | |
| await loadQuestion(prevId); | |
| } catch (err) { | |
| console.error(err); | |
| alert("์ด์ ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค."); | |
| } | |
| }); | |
| // ํ์ผ๋ก ์ด๋ | |
| homeBtn.addEventListener("click", () => { | |
| window.location.href = "/"; | |
| }); | |
| // ๊ฑด๋๋ฐ๊ธฐ (๋ต ํ์ธ ์์ด ๋ค์ ๋ฌธ์ ) | |
| skipBtn.addEventListener("click", async () => { | |
| try { | |
| const res = await fetch(`/api/next?current_id=${currentQuestion.id}`); | |
| const data = await res.json(); | |
| if (data.end) { | |
| alert("๋ง์ง๋ง ๋ฌธ์ ์ ๋๋ค!"); | |
| return; | |
| } | |
| currentQuestion = data; | |
| render(data); | |
| saveLastQuestionId(data.id); | |
| } catch (err) { | |
| console.error(err); | |
| alert("๋ค์ ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค."); | |
| } | |
| }); | |
| // ๋ฌธ์ ๋ฒํธ๋ก ์ด๋ | |
| jumpBtn.addEventListener("click", async () => { | |
| const targetId = parseInt(jumpInput.value); | |
| if (!targetId || targetId < 1) { | |
| alert("์ฌ๋ฐ๋ฅธ ๋ฌธ์ ๋ฒํธ๋ฅผ ์ ๋ ฅํ์ธ์."); | |
| return; | |
| } | |
| if (currentQuestion && targetId > currentQuestion.total) { | |
| alert(`๋ฌธ์ ๋ฒํธ๋ 1๋ถํฐ ${currentQuestion.total}๊น์ง์ ๋๋ค.`); | |
| return; | |
| } | |
| await loadQuestion(targetId); | |
| jumpInput.value = ""; | |
| }); | |
| // Enter ํค๋ก ๋ฌธ์ ์ด๋ | |
| jumpInput.addEventListener("keypress", (e) => { | |
| if (e.key === "Enter") { | |
| jumpBtn.click(); | |
| } | |
| }); | |
| // ๋ณต์ต ์ถ๊ฐ | |
| 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(); | |
| alert(data.message); | |
| } catch (err) { | |
| console.error(err); | |
| alert("๋ณต์ต๋ ธํธ ์ถ๊ฐ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."); | |
| } | |
| }); | |
| // ํค๋ณด๋ ๋จ์ถํค | |
| window.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !submitBtn.disabled && submitBtn.style.display !== "none") { | |
| submitBtn.click(); | |
| } | |
| if (e.key === "ArrowRight" && nextBtn.style.display !== "none") { | |
| nextBtn.click(); | |
| } | |
| if (e.key === "ArrowLeft") { | |
| prevBtn.click(); | |
| } | |
| }); | |
| // ํ์ด์ง ๋ก๋์ ๋ง์ง๋ง ๋ฌธ์ ๋๋ ์ฒซ ๋ฌธ์ | |
| const lastId = getLastQuestionId(); | |
| if (lastId) { | |
| loadQuestion(lastId); | |
| } else { | |
| loadQuestion(); | |
| } | |
| </script> | |
| </body> | |
| </html> |