github-actions
Sync from GitHub d2682fe6d3fcffe93aa302c286320962009f6436
9381502
Raw
History Blame Contribute Delete
13.9 kB
<!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;
}
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>