const STORAGE_KEY = 'studyapp_state'; const CONFIG = { DATASETS: { "enshu1": { Q_DATA: 'https://study-image-api.s-i-19921029.workers.dev/data/enshu1/questions', E_DATA: 'https://study-image-api.s-i-19921029.workers.dev/data/enshu1/explanations' }, "enshu2": { Q_DATA: 'https://study-image-api.s-i-19921029.workers.dev/data/enshu2/questions', E_DATA: 'https://study-image-api.s-i-19921029.workers.dev/data/enshu2/explanations' }, "enshu3": { Q_DATA: 'https://study-image-api.s-i-19921029.workers.dev/data/enshu3/questions', E_DATA: 'https://study-image-api.s-i-19921029.workers.dev/data/enshu3/explanations' } }, IMAGE_API: 'https://study-image-api.s-i-19921029.workers.dev/assets/image/' }; let appPassword = null; let state = { questions: [], explanations: [], currentList: [], currentIndex: 0, results: {}, mode: 'normal', lastViewedQuestionId: null, dataset: "enshu2" // ★追加 }; const dom = { qNumber: null, qText: document.getElementById('question-text'), choices: document.getElementById('choices-container'), exp: document.getElementById('explanation-container'), form: document.getElementById('answer-form'), validMsg: document.getElementById('validation-msg'), scoreC: document.getElementById('correct-count'), scoreW: document.getElementById('wrong-count'), scoreP: document.getElementById('score-percent'), resumeBtn: document.getElementById('resume-btn'), currentIdx: document.getElementById('current-idx'), totalIdx: document.getElementById('total-idx'), imageContainer: document.getElementById('image-container') }; function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify({ mode: state.mode, results: state.results, currentIndex: state.currentIndex, lastViewedQuestionId: state.lastViewedQuestionId ?? null, dataset: state.dataset })); } function loadState() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; try { const saved = JSON.parse(raw); state.mode = saved.mode || 'normal'; state.results = saved.results || {}; state.currentIndex = saved.currentIndex || 0; state.lastViewedQuestionId = saved.lastViewedQuestionId ?? null; state.dataset = saved.dataset || "enshu2"; } catch { } } function setupAuth() { const btn = document.getElementById("auth-btn"); const status = document.getElementById("auth-status"); btn.onclick = async () => { const pass = document.getElementById("auth-password").value; if (!pass) return; appPassword = pass; status.textContent = "認証済"; document.getElementById("auth-area").classList.add('hidden'); const ds = CONFIG.DATASETS[state.dataset]; const [qRes, eRes] = await Promise.all([ fetch(ds.Q_DATA, { headers: { "X-Auth-Password": appPassword } }), fetch(ds.E_DATA, { headers: { "X-Auth-Password": appPassword } }) ]); state.questions = await qRes.json(); state.explanations = await eRes.json(); buildCurrentList(); await render(); }; } async function init() { setupAuth(); setupModeButtons(); setupNavButtons(); document.getElementById('reset-score-btn').onclick = resetScore; loadState(); // ★ dataset復元をUIへ反映 const datasetSelect = document.getElementById('dataset-select'); if (datasetSelect) { datasetSelect.value = state.dataset; } buildCurrentList(); if (state.currentIndex >= state.currentList.length) { state.currentIndex = 0; } updateResumeButton(); await render(); datasetSelect.onchange = async (e) => { state.dataset = e.target.value; state.currentIndex = 0; state.results = {}; state.lastViewedQuestionId = null; saveState(); if (!appPassword) return; const ds = CONFIG.DATASETS[state.dataset]; const [qRes, eRes] = await Promise.all([ fetch(ds.Q_DATA, { headers: { "X-Auth-Password": appPassword } }), fetch(ds.E_DATA, { headers: { "X-Auth-Password": appPassword } }) ]); state.questions = await qRes.json(); state.explanations = await eRes.json(); buildCurrentList(); updateScore(); updateResumeButton(); await render(); }; } function setupModeButtons() { const nm = document.getElementById('normal-mode-btn'); const wm = document.getElementById('wrong-only-btn'); const sm = document.getElementById('shuffle-mode-btn'); async function activate(btn, text, mode) { document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.getElementById('mode-indicator').textContent = text; state.mode = mode; state.currentList = []; // ★ shuffle再生成用に空にする buildCurrentList(); state.currentIndex = 0; saveState(); updateResumeButton(); await render(); } nm.onclick = () => activate(nm, '現在のモード:通常', 'normal'); wm.onclick = () => activate(wm, '現在のモード:不正解のみ', 'wrong'); sm.onclick = () => activate(sm, '現在のモード:ランダム', 'shuffle'); // ★ 前回から再開 dom.resumeBtn.onclick = async () => { if (!state.lastViewedQuestionId) return; state.mode = 'normal'; buildCurrentList(); const idx = state.currentList.findIndex( q => q.question_id === state.lastViewedQuestionId ); if (idx !== -1) { state.currentIndex = idx; await render(); } }; } function updateResumeButton() { dom.resumeBtn.disabled = state.lastViewedQuestionId == null; } function setupNavButtons() { ['prev-btn', 'prev-btn-top'].forEach(id => { document.getElementById(id).onclick = async () => { if (state.currentIndex > 0) { state.currentIndex--; saveState(); await render(); } }; }); ['next-btn', 'next-btn-top'].forEach(id => { document.getElementById(id).onclick = async () => { if (state.currentIndex < state.currentList.length - 1) { state.currentIndex++; saveState(); await render(); } }; }); } function shuffleArray(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function buildCurrentList() { let base = state.mode === 'wrong' ? state.questions.filter(q => state.results[q.question_id] === false) : state.questions; if (state.mode === 'shuffle') { // ★ 既存リストを保持(再生成しない) if (!state.currentList.length) { state.currentList = shuffleArray(base); } } else { state.currentList = [...base]; } // ★ index安全化 if (state.currentIndex >= state.currentList.length) { state.currentIndex = 0; } } async function render() { const list = state.currentList; if (!dom.qNumber) { dom.qNumber = document.createElement('span'); dom.qNumber.className = 'question-number'; // dom.qText.before(dom.qNumber); dom.totalIdx.after(dom.qNumber); } dom.form.reset(); dom.choices.innerHTML = ''; dom.exp.classList.add('hidden'); dom.validMsg.classList.add('hidden'); if (!list.length) { dom.qText.textContent = '出題できる問題がありません'; dom.qNumber.style.display = 'none'; return; } const q = list[state.currentIndex]; // ★ 通常モードのみ履歴保存 if (state.mode === 'normal') { state.lastViewedQuestionId = q.question_id; saveState(); updateResumeButton(); } dom.qNumber.style.display = 'inline'; dom.qNumber.textContent = `問題ID:${q.question_id}`; // ★ 問題画像表示処理 if (q.question_image) { if (!appPassword) { dom.imageContainer.innerHTML = "パスワード認証が必要です"; dom.qText.style.display = 'none'; } else { const imageId = q.question_image.replace('.jpg', ''); const res = await fetch( CONFIG.IMAGE_API + imageId, { headers: { "X-Auth-Password": appPassword } } ); if (!res.ok) { // ★ 画像読み込み失敗時 dom.imageContainer.style.display = 'none'; dom.qText.style.display = 'block'; dom.qText.textContent = q.question_text; // dom.imageContainer.innerHTML = "画像取得失敗"; } else { const blob = await res.blob(); const imgUrl = URL.createObjectURL(blob); dom.imageContainer.innerHTML = ''; const img = document.createElement('img'); img.src = imgUrl; img.className = 'question-img'; dom.qText.style.display = 'none'; dom.imageContainer.appendChild(img); dom.imageContainer.style.display = 'block'; } } } else { dom.imageContainer.innerHTML = ''; dom.imageContainer.style.display = 'none'; dom.qText.style.display = 'block'; dom.qText.textContent = q.question_text; } // ★ answer_type に応じて input type 切替 const inputType = q.answer_type === 'multiple' ? 'checkbox' : 'radio'; q.choices.forEach(c => { const label = document.createElement('label'); label.className = 'choice-item'; const input = document.createElement('input'); input.type = inputType; input.name = 'ans'; input.value = c.label; label.appendChild(input); const text = ` ${c.label}: ${c.text}`; const parts = text.split('\n'); const textSpan = document.createElement('span'); textSpan.className = 'choice-text'; parts.forEach((line, index) => { textSpan.appendChild(document.createTextNode(line)); if (index < parts.length - 1) { textSpan.appendChild(document.createElement('br')); } }); label.appendChild(textSpan); dom.choices.appendChild(label); }); dom.currentIdx.textContent = state.currentIndex + 1; dom.totalIdx.textContent = list.length + " "; } // ======================== // 回答処理 // ======================== dom.form.onsubmit = e => { e.preventDefault(); const selected = Array.from(new FormData(dom.form).getAll('ans')); if (!selected.length) { dom.validMsg.classList.remove('hidden'); return; } const q = state.currentList[state.currentIndex]; const ex = state.explanations.find(e => e.question_id === q.question_id); const correct = ex.correct_answers.sort(); const isCorrect = JSON.stringify(selected.sort()) === JSON.stringify(correct); state.results[q.question_id] = isCorrect; saveState(); // ★追加 document.querySelectorAll('.choice-item').forEach(l => { const v = l.querySelector('input').value; if (!isCorrect && correct.includes(v)) { l.classList.add('correct-highlight'); } }); dom.exp.classList.remove('hidden'); dom.exp.className = isCorrect ? 'correct-ui' : 'wrong-ui'; document.getElementById('result-badge').textContent = isCorrect ? '✓ 正解' : '× 不正解'; document.getElementById('correct-answer-text').textContent = correct.join(', '); document.getElementById('explanation-text').textContent = ex.explanation_text; updateScore(); }; // ======================== // スコア // ======================== function updateScore() { const v = Object.values(state.results); dom.scoreC.textContent = v.filter(x => x).length; dom.scoreW.textContent = v.filter(x => !x).length; dom.scoreP.textContent = state.questions.length ? Math.round((v.filter(x => x).length / state.questions.length) * 100) : 0; } function resetScore() { state.results = {}; saveState(); updateScore(); buildCurrentList(); if (state.currentIndex >= state.currentList.length) { state.currentIndex = 0; } render(); } init();