← 실습 자료

원본 퀴즈쇼 index.html

발표자료와 함께 제공된 기본 실행 HTML입니다.

HTML 복사하기

HTML 직접 보기
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>우리 반 퀴즈쇼</title>
    <meta name="description" content="교실 전자칠판과 태블릿을 위한 화려하고 재미있는 우리 반 퀴즈쇼!">
    <style>
        button:focus-visible, input:focus-visible, textarea:focus-visible {
            outline: 3px solid #facc15;
            outline-offset: 3px;
        }
        :root {
            --bg-deep: #0f172a;
            --header-bg: #1e293b;
            --text-yellow: #facc15;
            --text-white: #ffffff;
            --coral: #fb7185;
            --mint: #34d399;
            --blue: #60a5fa;
            --lavender: #a78bfa;
            --success: #22c55e;
            --danger: #ef4444;
            --panel-bg: rgba(30, 41, 59, 0.95);
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: 'Pretendard', 'Apple SD Gothic Neo', sans-serif;
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }

        body {
            background-color: var(--bg-deep);
            color: var(--text-white);
            min-height: 100vh;
            overflow-x: hidden; overflow-y: auto;
            display: flex;
            flex-direction: column;
        }

        /* Confetti Canvas */
        #confetti {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 1000;
        }

        /* Header */
        header {
            padding: 15px 25px;
            background: var(--header-bg);
            display: flex;
            justify-content: space-between;
            align-items: center;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3);
            z-index: 10;
        }

        .logo-area {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .cloud-logo {
            position: relative;
            width: 40px;
            height: 25px;
            background: var(--lavender);
            border-radius: 50px;
        }
        .cloud-logo::before, .cloud-logo::after {
            content: '';
            position: absolute;
            background: var(--lavender);
            border-radius: 50%;
        }
        .cloud-logo::before { width: 20px; height: 20px; top: -10px; left: 6px; }
        .cloud-logo::after { width: 15px; height: 15px; top: -6px; left: 20px; }

        .logo-text {
            font-weight: 900;
            font-size: 1.4rem;
            color: var(--lavender);
            letter-spacing: -1px;
        }

        .tool-bar {
            display: flex;
            gap: 10px;
        }

        .tool-btn {
            background: rgba(255,255,255,0.1);
            border: 1px solid rgba(255,255,255,0.2);
            color: white;
            padding: 8px 15px;
            border-radius: 50px;
            cursor: pointer;
            font-size: 0.9rem;
            display: flex;
            align-items: center;
            gap: 5px;
            transition: all 0.2s;
        }
        .tool-btn:hover { background: rgba(255,255,255,0.2); }
        .tool-btn.active { background: var(--lavender); border-color: var(--lavender); }

        /* Main Content */
        main {
            flex: 1;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 20px;
            position: relative;
            z-index: 5;
        }

        .quiz-container {
            width: 100%;
            max-width: 1000px;
            text-align: center;
        }

        .title-board {
            margin-bottom: 30px;
            animation: bounceIn 0.8s ease;
        }

        .quiz-title {
            font-size: 4.5rem;
            font-weight: 900;
            color: var(--text-yellow);
            text-shadow: 4px 4px 0px rgba(0,0,0,0.5), 0 0 20px rgba(250, 204, 21, 0.3);
            margin-bottom: 10px;
            letter-spacing: -2px;
        }

        .status-line {
            display: flex;
            justify-content: center;
            gap: 30px;
            font-size: 1.8rem;
            font-weight: 700;
        }

        .status-badge {
            background: rgba(0,0,0,0.3);
            padding: 10px 25px;
            border-radius: 100px;
            border: 2px solid var(--text-yellow);
            color: var(--text-yellow);
        }

        .question-card {
            background: rgba(255,255,255,0.05);
            padding: 40px;
            border-radius: 40px;
            border: 4px solid rgba(255,255,255,0.1);
            margin-bottom: 40px;
            backdrop-filter: blur(10px);
        }

        .question-text {
            font-size: 3rem;
            line-height: 1.2;
            word-break: keep-all;
            margin-bottom: 40px;
        }

        /* Options */
        .options-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 25px;
        }

        .option-btn {
            position: relative;
            padding: 40px 20px;
            font-size: 2.2rem;
            font-weight: 900;
            color: white;
            border: none;
            border-radius: 30px;
            cursor: pointer;
            transition: all 0.1s;
            box-shadow: 0 12px 0 var(--shadow-color);
            transform: translateY(0);
        }

        .option-btn:active:not(:disabled) {
            transform: translateY(8px);
            box-shadow: 0 4px 0 var(--shadow-color);
        }

        .option-btn.selected {
            outline: 6px solid var(--text-yellow);
            outline-offset: 4px;
        }

        .option-btn.selected::after {
            content: '✓';
            position: absolute;
            top: 10px;
            right: 15px;
            font-size: 2rem;
            text-shadow: 0 2px 4px rgba(0,0,0,0.3);
        }

        .btn-coral { background: var(--coral); --shadow-color: #e11d48; }
        .btn-mint { background: var(--mint); --shadow-color: #059669; }
        .btn-blue { background: var(--blue); --shadow-color: #2563eb; }
        .btn-lavender { background: var(--lavender); --shadow-color: #7c3aed; }

        .option-btn:disabled {
            cursor: default;
            opacity: 0.6;
        }

        /* Actions */
        .action-bar {
            margin-top: 30px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 20px;
        }

        .reveal-btn {
            background: var(--text-yellow);
            color: #000;
            font-size: 2.5rem;
            font-weight: 900;
            padding: 20px 80px;
            border-radius: 100px;
            border: none;
            cursor: pointer;
            box-shadow: 0 8px 0 #ca8a04, 0 10px 25px rgba(250, 204, 21, 0.4);
            transition: all 0.1s;
        }
        .reveal-btn:active {
            transform: translateY(4px);
            box-shadow: 0 4px 0 #ca8a04;
        }
        .reveal-btn:disabled {
            background: #94a3b8;
            box-shadow: 0 8px 0 #64748b;
            cursor: not-allowed;
            color: #475569;
        }

        /* Result Overlay */
        .result-panel {
            background: var(--panel-bg);
            border: 4px solid var(--text-yellow);
            padding: 40px;
            border-radius: 40px;
            display: none;
            flex-direction: column;
            align-items: center;
            animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
            max-width: 800px;
            width: 100%;
        }
        .result-panel.show { display: flex; }

        .result-title {
            font-size: 4rem;
            font-weight: 900;
            margin-bottom: 20px;
        }

        .explanation {
            font-size: 2rem;
            color: #cbd5e1;
            line-height: 1.4;
            margin-bottom: 30px;
            word-break: keep-all;
        }

        /* Final View */
        .final-view {
            display: none;
            flex-direction: column;
            align-items: center;
            gap: 30px;
        }
        .final-view.show { display: flex; }

        .final-score {
            font-size: 8rem;
            font-weight: 900;
            color: var(--text-yellow);
            text-shadow: 0 0 40px rgba(250, 204, 21, 0.5);
        }

        /* Editor Modal */
        .modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.8);
            z-index: 2000;
            display: none;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .modal-content {
            background: var(--header-bg);
            width: 100%;
            max-width: 900px;
            max-height: 90vh;
            border-radius: 40px;
            padding: 40px;
            overflow-y: auto;
            border: 4px solid var(--lavender);
        }

        .editor-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 30px;
        }

        .q-edit-card {
            background: rgba(255,255,255,0.05);
            padding: 25px;
            border-radius: 20px;
            margin-bottom: 20px;
            border-left: 10px solid var(--lavender);
        }

        .input-group {
            margin-bottom: 15px;
        }
        .input-group label {
            display: block;
            margin-bottom: 8px;
            font-weight: 700;
            color: var(--lavender);
        }
        .input-field {
            width: 100%;
            background: rgba(0,0,0,0.3);
            border: 2px solid rgba(255,255,255,0.1);
            color: white;
            padding: 12px 15px;
            border-radius: 12px;
            font-size: 1.1rem;
        }
        .input-field:focus {
            border-color: var(--lavender);
            outline: none;
        }

        .error-msg {
            color: var(--danger);
            font-weight: 700;
            margin-bottom: 15px;
            padding: 10px;
            background: rgba(239, 68, 68, 0.1);
            border-radius: 10px;
            display: none;
        }

        /* Footer */
        footer {
            padding: 15px;
            text-align: right;
            z-index: 10;
        }

        /* Animations */
        @keyframes bounceIn {
            0% { transform: scale(0.3); opacity: 0; }
            50% { transform: scale(1.05); opacity: 1; }
            70% { transform: scale(0.9); }
            100% { transform: scale(1); }
        }

        @keyframes popIn {
            0% { transform: scale(0.8); opacity: 0; }
            100% { transform: scale(1); opacity: 1; }
        }

        .sparkle {
            animation: sparkleAnim 0.6s ease-in-out infinite;
        }
        @keyframes sparkleAnim {
            0%, 100% { transform: scale(1); filter: brightness(1); }
            50% { transform: scale(1.1); filter: brightness(1.5); }
        }

        /* Tablet & Mobile adjustment */
        @media (max-width: 850px) {
            .quiz-title { font-size: 3rem; }
            .question-text { font-size: 2rem; }
            .option-btn { font-size: 1.6rem; padding: 30px 15px; }
            .reveal-btn { font-size: 2rem; padding: 15px 50px; }
            .status-line { font-size: 1.3rem; }
        }

        @media (max-height: 700px) {
            .quiz-title { font-size: 2.5rem; margin-bottom: 5px; }
            .question-card { padding: 20px; margin-bottom: 15px; }
            .question-text { font-size: 1.8rem; margin-bottom: 20px; }
            .option-btn { padding: 20px 10px; font-size: 1.4rem; }
            header { padding: 8px 15px; }
        }

        /* Scrollbar */
        ::-webkit-scrollbar { width: 8px; }
        ::-webkit-scrollbar-track { background: transparent; }
        ::-webkit-scrollbar-thumb { background: var(--lavender); border-radius: 10px; }
        body.reduce-motion *, body.reduce-motion *::before, body.reduce-motion *::after { animation: none !important; transition: none !important; }
        .option-btn { color: #102038; min-height: 64px; overflow-wrap: anywhere; }
        .option-btn:disabled { opacity: .9; }
        .tool-btn { min-height: 44px; }
        main { padding-bottom: 72px; }
        @media (max-width: 1100px) {
          .quiz-title { font-size: clamp(2rem, 5vw, 3rem); }
          .question-text { font-size: clamp(1.6rem, 3.5vw, 2.2rem); margin-bottom: 24px; }
          .question-card { padding: 24px; margin-bottom: 20px; }
          .option-btn { padding: 22px 14px; font-size: 1.7rem; }
          .title-board { margin-bottom: 22px; }
        }
        @media (max-width: 600px) {
          header { flex-wrap: wrap; gap: 16px; padding: 18px; }
          .status-line { gap: 12px; }
          .status-badge { padding: 8px 16px; }
          .options-grid { gap: 18px; }
          .question-card { padding: 18px; }
          .reveal-btn { max-width: 100%; padding: 15px 26px; }
        }
    </style>
</head>
<body>

<canvas id="confetti"></canvas>

<header>
    <div class="logo-area">
        <div class="cloud-logo"></div>
        <div class="logo-text">에듀잇티</div>
    </div>
    <div class="tool-bar">
        <button id="soundToggle" class="tool-btn" onclick="toggleSound()">🔇 소리 켜기</button>
        <button id="motionToggle" class="tool-btn" onclick="toggleMotion()">✨ 움직임 줄이기</button>
        <button id="fullscreenBtn" class="tool-btn" onclick="toggleFullscreen()">🖥️ 전체화면</button>
    </div>
</header>

<main>
    <div id="quizView" class="quiz-container">
        <div class="title-board">
            <h1 class="quiz-title">우리 반 퀴즈쇼</h1>
            <div class="status-line">
                <div class="status-badge">문제 <span id="qIdx">1</span> / <span id="qTotal">3</span></div>
                <div class="status-badge" style="border-color: white; color: white;">점수: <span id="currentScore">0</span></div>
            </div>
        </div>

        <div id="quizContent">
            <div class="question-card">
                <h2 id="questionText" class="question-text">질문을 불러오는 중...</h2>
                <div id="selectionAlert" style="color: var(--danger); font-weight: 900; margin-bottom: 15px; visibility: hidden; font-size: 1.5rem;">보기를 먼저 선택해 주세요!</div>
                <div id="optionsGrid" class="options-grid">
                    <!-- Buttons Injected -->
                </div>
            </div>

            <div class="action-bar">
                <button id="revealBtn" class="reveal-btn" onclick="handleReveal()">정답 공개</button>

                <div id="resultPanel" class="result-panel" aria-live="polite">
                    <div id="resultTitle" class="result-title">정답입니다!</div>
                    <p id="explanationText" class="explanation">해설이 표시됩니다.</p>
                    <button id="nextBtn" class="reveal-btn" onclick="goToNext()">다음 문제</button>
                </div>
            </div>
        </div>

        <div id="finalView" class="final-view">
            <h2 style="font-size: 4rem;">축하합니다!</h2>
            <div class="final-score"><span id="finalScoreDisplay">0</span>점</div>
            <button class="reveal-btn" onclick="restartQuiz()">다시 시작하기</button>
        </div>
    </div>
</main>

<footer>
    <button class="tool-btn" onclick="openEditor()">⚙️ 문제 편집</button>
</footer>

<!-- Editor Modal -->
<div id="editorModal" class="modal">
    <div class="modal-content">
        <div class="editor-header">
            <h2 style="font-size: 2.5rem; color: var(--text-yellow);">문제 편집기</h2>
            <button class="tool-btn" onclick="closeEditor()">닫기</button>
        </div>

        <div id="editorError" class="error-msg">빈칸 없이 보기 4개와 정답 번호 1~4를 입력해주세요!</div>

        <div id="editorList">
            <!-- Questions List -->
        </div>

        <div style="display: flex; gap: 20px; justify-content: center; margin-top: 30px;">
            <button class="reveal-btn" style="background: var(--lavender); box-shadow: 0 8px 0 #7c3aed; font-size: 1.8rem;" onclick="applyEdits()">변경사항 적용</button>
            <button class="reveal-btn" style="background: var(--header-bg); border: 2px solid white; color: white; box-shadow: 0 8px 0 #000; font-size: 1.8rem;" onclick="closeEditor()">취소</button>
        </div>
        <p style="margin-top: 20px; text-align: center; color: var(--secondary);">* 새로고침하면 기본 문제로 돌아갑니다.</p>
    </div>
</div>

<script>
    // --- Audio System ---
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    let audioCtx = null;
    let isSoundOn = false;

    function playTone(freq, duration, type = 'sine', volume = 0.1) {
        if (!isSoundOn || !audioCtx) return;
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
        gain.gain.setValueAtTime(volume, audioCtx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
        osc.connect(gain);
        gain.connect(audioCtx.destination);
        osc.start();
        osc.stop(audioCtx.currentTime + duration);
    }

    function soundSelect() { playTone(660, 0.1, 'square', 0.05); }
    function soundCorrect() {
        playTone(523.25, 0.2, 'triangle', 0.1);
        setTimeout(() => playTone(659.25, 0.2, 'triangle', 0.1), 100);
        setTimeout(() => playTone(783.99, 0.4, 'triangle', 0.1), 200);
    }
    function soundIncorrect() {
        playTone(220, 0.3, 'sawtooth', 0.1);
        setTimeout(() => playTone(110, 0.5, 'sawtooth', 0.1), 100);
    }
    function soundDrumroll() {
        if (!isSoundOn) return;
        const duration = 1.5;
        const bufferSize = audioCtx.sampleRate * duration;
        const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
        const noise = audioCtx.createBufferSource();
        noise.buffer = buffer;
        const gain = audioCtx.createGain();
        gain.gain.setValueAtTime(0.01, audioCtx.currentTime);
        gain.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + duration);
        gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + duration + 0.1);
        noise.connect(gain);
        gain.connect(audioCtx.destination);
        noise.start();
    }
    function soundFanfare() {
        const notes = [523, 659, 784, 1046];
        notes.forEach((f, i) => setTimeout(() => playTone(f, 0.4, 'square', 0.05), i * 150));
    }

    // --- Confetti System ---
    const canvas = document.getElementById('confetti');
    const ctx = canvas.getContext('2d');
    let particles = [];
    let isMotionOn = true;

    function resizeCanvas() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    }
    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();

    class Particle {
        constructor() {
            this.x = Math.random() * canvas.width;
            this.y = -10;
            this.size = Math.random() * 10 + 5;
            this.color = [`#fb7185`, `#34d399`, `#60a5fa`, `#facc15`][Math.floor(Math.random() * 4)];
            this.speedX = Math.random() * 4 - 2;
            this.speedY = Math.random() * 5 + 3;
            this.rot = Math.random() * Math.PI * 2;
            this.rotSpeed = Math.random() * 0.2 - 0.1;
        }
        update() {
            this.x += this.speedX;
            this.y += this.speedY;
            this.rot += this.rotSpeed;
        }
        draw() {
            ctx.save();
            ctx.translate(this.x, this.y);
            ctx.rotate(this.rot);
            ctx.fillStyle = this.color;
            ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
            ctx.restore();
        }
    }

    function spawnConfetti() {
        if (!isMotionOn) return;
        particles = [];
        for (let i = 0; i < 100; i++) particles.push(new Particle());
        animateConfetti();
    }

    function animateConfetti() {
        if (particles.length === 0) return;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        particles.forEach((p, i) => {
            p.update();
            p.draw();
            if (p.y > canvas.height) particles.splice(i, 1);
        });
        requestAnimationFrame(animateConfetti);
    }

    // --- Quiz Data & Logic ---
    const defaultData = [
        {
            question: "삼각형의 변은 몇 개일까요?",
            options: ["2개", "3개", "4개", "5개"],
            correct: 1,
            explanation: "삼각형은 변이 3개인 다각형입니다!"
        },
        {
            question: "1/2과 크기가 같은 분수는?",
            options: ["1/3", "2/4", "2/3", "3/4"],
            correct: 1,
            explanation: "분모와 분자에 같은 수를 곱하면 2/4가 돼요!"
        },
        {
            question: "1m는 몇 cm일까요?",
            options: ["10", "50", "100", "1000"],
            correct: 2,
            explanation: "1미터는 정확히 100센티미터입니다!"
        }
    ];

    let questions = JSON.parse(JSON.stringify(defaultData));
    let currentIdx = 0;
    let score = 0;
    let selectedIdx = null;
    let isRevealed = false;
    let revealVersion = 0;

    const colors = ['coral', 'mint', 'blue', 'lavender'];

    function renderQuestion() {
        revealVersion++;
        const q = questions[currentIdx];
        document.getElementById('qIdx').innerText = currentIdx + 1;
        document.getElementById('qTotal').innerText = questions.length;
        document.getElementById('currentScore').innerText = score;
        document.getElementById('questionText').innerText = q.question;
        document.getElementById('selectionAlert').style.visibility = 'hidden';
        document.getElementById('resultPanel').classList.remove('show');
        document.getElementById('quizContent').style.display = 'block';
        document.getElementById('finalView').classList.remove('show');
        document.getElementById('revealBtn').style.display = 'block';
        document.getElementById('revealBtn').disabled = false;

        const grid = document.getElementById('optionsGrid');
        grid.innerHTML = '';
        q.options.forEach((opt, i) => {
            const btn = document.createElement('button');
            btn.className = `option-btn btn-${colors[i % 4]}`;
            btn.innerText = opt;
            btn.onclick = () => selectOption(i, btn);
            grid.appendChild(btn);
        });

        isRevealed = false;
        selectedIdx = null;
    }

    function selectOption(idx, btn) {
        if (isRevealed) return;
        selectedIdx = idx;
        soundSelect();
        document.getElementById('selectionAlert').style.visibility = 'hidden';
        document.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected'));
        btn.classList.add('selected');
    }

    async function handleReveal() {
        if (isRevealed) return;
        if (selectedIdx === null) {
            document.getElementById('selectionAlert').style.visibility = 'visible';
            return;
        }

        const btn = document.getElementById('revealBtn');
        btn.disabled = true;
        isRevealed = true;
        const version = revealVersion;
        document.querySelectorAll('.option-btn').forEach(b => b.disabled = true);
        soundDrumroll();

        // Short delay for drama
        await new Promise(r => setTimeout(r, 1500));

        if (version !== revealVersion) return;
        const q = questions[currentIdx];
        const isCorrect = (selectedIdx === q.correct);

        if (isCorrect) {
            score += 100;
            soundCorrect();
            spawnConfetti();
        } else {
            soundIncorrect();
        }

        document.getElementById('currentScore').innerText = score;
        btn.style.display = 'none';

        const panel = document.getElementById('resultPanel');
        const title = document.getElementById('resultTitle');
        title.innerText = isCorrect ? "정답입니다! 🌟" : "아쉬워요! 💦";
        title.className = isCorrect ? 'result-title sparkle' : 'result-title';
        title.style.color = isCorrect ? 'var(--text-yellow)' : 'var(--danger)';
        document.getElementById('explanationText').innerText = q.explanation;
        panel.classList.add('show');

        // Visual feedback on options
        document.querySelectorAll('.option-btn').forEach((b, i) => {
            b.disabled = true;
            if (i === q.correct) {
                b.style.border = '6px solid gold';
                b.style.filter = 'brightness(1.2)';
            } else if (i === selectedIdx && !isCorrect) {
                b.style.opacity = '0.3';
            }
        });

        document.getElementById('nextBtn').innerText = (currentIdx === questions.length - 1) ? "최종 결과" : "다음 문제";
    }

    function goToNext() {
        if (currentIdx < questions.length - 1) {
            currentIdx++;
            renderQuestion();
        } else {
            showFinal();
        }
    }

    function showFinal() {
        document.getElementById('quizContent').style.display = 'none';
        document.getElementById('finalView').classList.add('show');
        document.getElementById('finalScoreDisplay').innerText = score;
        soundFanfare();
        spawnConfetti();
    }

    function restartQuiz() {
        currentIdx = 0;
        score = 0;
        renderQuestion();
    }

    // --- UI Controls ---
    function toggleSound() {
        if (!audioCtx) audioCtx = new AudioCtx();
        isSoundOn = !isSoundOn;
        const btn = document.getElementById('soundToggle');
        btn.innerText = isSoundOn ? "🔊 소리 끄기" : "🔇 소리 켜기";
        btn.classList.toggle('active', isSoundOn);
        if (isSoundOn && audioCtx.state === 'suspended') audioCtx.resume();
    }

    function toggleMotion() {
        isMotionOn = !isMotionOn;
        document.body.classList.toggle('reduce-motion', !isMotionOn);
        if (!isMotionOn) { particles.length = 0; ctx.clearRect(0, 0, canvas.width, canvas.height); }
        const btn = document.getElementById('motionToggle');
        btn.innerText = isMotionOn ? "✨ 움직임 줄이기" : "🚫 움직임 켜기";
        btn.classList.toggle('active', !isMotionOn);
    }

    function toggleFullscreen() {
        if (!document.fullscreenElement) {
            document.documentElement.requestFullscreen();
            document.getElementById('fullscreenBtn').innerText = "📴 화면 축소";
        } else {
            document.exitFullscreen();
            document.getElementById('fullscreenBtn').innerText = "🖥️ 전체화면";
        }
    }

    // --- Editor Modal ---
    function openEditor() {
        if (isRevealed && document.getElementById("revealBtn").style.display !== "none") return;
        const escapeAttr = value => String(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
        const list = document.getElementById('editorList');
        list.innerHTML = '';
        questions.forEach((q, i) => {
            const card = document.createElement('div');
            card.className = 'q-edit-card';
            card.innerHTML = `
                <div class="input-group">
                    <label>문제 ${i+1} 질문</label>
                    <input type="text" class="input-field q-txt" value="${escapeAttr(q.question)}">
                </div>
                <div class="input-group">
                    <label>보기 (쉼표로 구분)</label>
                    <input type="text" class="input-field q-opts" value="${escapeAttr(q.options.join(', '))}">
                </div>
                <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 15px;">
                    <div class="input-group">
                        <label>정답 번호 (1-4)</label>
                        <input type="number" class="input-field q-ans" value="${q.correct + 1}" min="1" max="4">
                    </div>
                    <div class="input-group">
                        <label>해설</label>
                        <input type="text" class="input-field q-exp" value="${escapeAttr(q.explanation)}">
                    </div>
                </div>
            `;
            list.appendChild(card);
        });
        document.getElementById('editorError').style.display = 'none';
        document.getElementById('editorModal').style.display = 'flex';
    }

    function closeEditor() {
        document.getElementById('editorModal').style.display = 'none';
    }

    function applyEdits() {
        const cards = document.querySelectorAll('.q-edit-card');
        const newQs = [];
        let isValid = true;

        cards.forEach(card => {
            const q = card.querySelector('.q-txt').value.trim();
            const optsRaw = card.querySelector('.q-opts').value.split(',').map(s => s.trim());
            const ans = Number(card.querySelector('.q-ans').value) - 1;
            const exp = card.querySelector('.q-exp').value.trim();

            if (!q || optsRaw.length !== 4 || optsRaw.some(v => !v) || !Number.isInteger(ans) || ans < 0 || ans > 3 || !exp) isValid = false;

            newQs.push({
                question: q,
                options: optsRaw,
                correct: ans,
                explanation: exp
            });
        });

        if (!isValid) {
            document.getElementById('editorError').style.display = 'block';
            return;
        }

        questions = newQs;
        closeEditor();
        restartQuiz();
    }

    // Init
    renderQuestion();
</script>

</body>
</html>