← 실습 자료

발표자 뽑기 실행 예시

가상학생 이름으로 뽑기를 실행합니다.

HTML 복사하기

HTML 직접 보기
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>두근두근 발표 뽑기</title>
<style>
  :root { color-scheme: light; font-family: system-ui, -apple-system, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; color: #172b4d; background: #eaf3ff; }
  * { box-sizing: border-box; }
  body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 75% 3%, #fff9d9, transparent 30%), #eaf3ff; }
  button, textarea { font: inherit; }
  button { cursor: pointer; min-height: 56px; border: 0; border-radius: 18px; font-weight: 800; }
  button:disabled { cursor: not-allowed; opacity: .55; }
  :focus-visible { outline: 4px solid #172b4d; outline-offset: 3px; }
  .shell { max-width: 1180px; margin: 0 auto; padding: clamp(18px, 3vw, 40px); }
  header { display: flex; justify-content: space-between; gap: 16px; align-items: start; }
  .eyebrow { margin: 0 0 8px; font-size: 13px; letter-spacing: .14em; font-weight: 900; color: #4263eb; }
  h1 { margin: 0; font-size: clamp(32px, 4.6vw, 58px); line-height: 1.13; letter-spacing: -.06em; }
  .subtitle { margin: 12px 0 0; color: #526582; font-size: clamp(15px, 1.8vw, 19px); }
  .sound { padding: 0 20px; color: #274166; background: #fff; border: 2px solid #d6e3ff; white-space: nowrap; }
  .layout { display: grid; grid-template-columns: minmax(260px, .8fr) minmax(350px, 1.4fr); gap: 22px; align-items: stretch; margin-top: 26px; }
  .panel, .stage { border-radius: 30px; box-shadow: 0 18px 45px #31519a1a; }
  .panel { padding: 24px; background: #fff; }
  .panel h2 { margin: 0 0 10px; font-size: 22px; }
  .hint { color: #526582; font-size: 14px; line-height: 1.5; }
  label { display: block; margin: 18px 0 8px; font-weight: 800; }
  textarea { width: 100%; min-height: 180px; resize: vertical; padding: 14px; border: 2px solid #cbd9f6; border-radius: 16px; color: #172b4d; line-height: 1.55; }
  .panel-actions { display: flex; gap: 9px; margin-top: 12px; }
  .panel-actions button { flex: 1; min-height: 48px; font-size: 14px; }
  .apply { color: #fff; background: #4263eb; }
  .ghost { background: #edf2ff; color: #274166; }
  .status { min-height: 26px; margin: 12px 0 0; color: #97501a; font-weight: 700; }
  .history { margin-top: 24px; padding-top: 18px; border-top: 1px solid #e4eaf6; }
  .history h3 { margin: 0 0 8px; font-size: 16px; }
  .history ol { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
  .history li { padding: 7px 11px; border-radius: 999px; background: #eef3ff; font-weight: 700; }
  .stage { position: relative; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: space-between; min-height: 560px; padding: 28px; text-align: center; color: #fff; background: linear-gradient(155deg, #4468ed, #3151c7); }
  .stage::before { content: ""; position: absolute; width: 520px; height: 520px; top: -330px; right: -200px; border-radius: 50%; background: #ffffff1a; }
  .remaining { position: relative; padding: 9px 16px; border-radius: 999px; background: #ffffff2c; font-weight: 800; }
  .machine { position: relative; width: min(300px, 74vw); height: 215px; margin: 24px 0 12px; border: 10px solid #d8e4ff; border-radius: 45% 45% 28% 28%; background: linear-gradient(#ffffff55, #ffffff18); box-shadow: inset 0 0 35px #fff8, 0 18px 0 #2441a8; }
  .capsule { position: absolute; width: 55px; height: 55px; border-radius: 50%; box-shadow: inset -7px -8px #172b4d19, 0 7px 10px #172b4d35; }
  .capsule:nth-child(1) { left: 24px; bottom: 25px; background: #ff8b76; }
  .capsule:nth-child(2) { left: 79px; bottom: 61px; background: #ffe28a; }
  .capsule:nth-child(3) { left: 130px; bottom: 21px; background: #9fe0c5; }
  .capsule:nth-child(4) { left: 190px; bottom: 67px; background: #c8bcf6; }
  .capsule:nth-child(5) { left: 208px; bottom: 12px; background: #ff8b76; }
  .stage.mixing .capsule { animation: wobble .22s ease-in-out 3 alternate; }
  .result-label { position: relative; margin: 0; opacity: .83; font-size: 15px; font-weight: 800; }
  .result { position: relative; min-height: 76px; margin: 6px 0 17px; font-size: clamp(36px, 6vw, 70px); line-height: 1.2; font-weight: 900; letter-spacing: -.06em; }
  .draw { position: relative; min-width: min(100%, 360px); padding: 12px 28px; color: #172b4d; background: #ffe28a; box-shadow: 0 8px 0 #d9b851; font-size: clamp(19px, 2.3vw, 25px); }
  .draw:active:not(:disabled) { transform: translateY(5px); box-shadow: 0 3px 0 #d9b851; }
  .micro { position: relative; margin: 20px 0 0; font-size: 13px; color: #e5edff; }
  @keyframes wobble { from { transform: translate(-5px, 5px) rotate(-10deg); } to { transform: translate(8px, -8px) rotate(13deg); } }
  @media (max-width: 780px) { header { flex-direction: column; } .layout { grid-template-columns: 1fr; } .stage { min-height: 500px; } }
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } }
</style>
</head>
<body>
<main class="shell">
  <header>
    <div><p class="eyebrow">CLASSROOM PICKER</p><h1>두근두근 발표 뽑기</h1><p class="subtitle">명단을 적용하고, 남은 학생 중 한 명을 뽑아 보세요.</p></div>
    <button class="sound" id="sound" type="button" aria-pressed="false">소리 꺼짐</button>
  </header>
  <div class="layout">
    <section class="panel" aria-labelledby="list-title">
      <h2 id="list-title">오늘의 명단</h2>
      <p class="hint">연수에서는 가상 이름만 사용합니다. 쉼표나 줄바꿈으로 구분해 넣으세요.</p>
      <label for="names">뽑을 이름</label>
      <textarea id="names" spellcheck="false">가상학생 01
가상학생 02
가상학생 03
가상학생 04
가상학생 05</textarea>
      <div class="panel-actions"><button class="apply" id="apply" type="button">명단 적용</button><button class="ghost" id="reset" type="button">다시 시작</button></div>
      <p class="status" id="status" role="status" aria-live="polite"></p>
      <div class="history"><h3>뽑힌 순서</h3><ol id="history"><li>아직 뽑지 않았어요</li></ol></div>
    </section>
    <section class="stage" id="stage" aria-labelledby="stage-title">
      <span class="remaining" id="remaining">남은 학생 5명</span>
      <div class="machine" aria-hidden="true"><span class="capsule"></span><span class="capsule"></span><span class="capsule"></span><span class="capsule"></span><span class="capsule"></span></div>
      <div><p class="result-label" id="stage-title">오늘의 주인공</p><p class="result" id="result" aria-live="polite">누가 나올까요?</p><button class="draw" id="draw" type="button">두근두근 뽑기!</button></div>
      <p class="micro">중복 없이 뽑기 · 소리는 기본 꺼짐 · 이 화면에만 결과 표시</p>
    </section>
  </div>
</main>
<script>
(() => {
  const namesInput = document.getElementById('names');
  const status = document.getElementById('status');
  const remainingLabel = document.getElementById('remaining');
  const result = document.getElementById('result');
  const history = document.getElementById('history');
  const stage = document.getElementById('stage');
  const drawButton = document.getElementById('draw');
  const soundButton = document.getElementById('sound');
  const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
  let names = ['가상학생 01', '가상학생 02', '가상학생 03', '가상학생 04', '가상학생 05'];
  let pool = names.slice();
  let picked = [];
  let busy = false;
  let soundOn = false;
  let audioContext;

  function playTone(frequency, duration) {
    if (!soundOn) return;
    try {
      const AudioEngine = window.AudioContext || window.webkitAudioContext;
      if (!AudioEngine) return;
      audioContext = audioContext || new AudioEngine();
      const oscillator = audioContext.createOscillator();
      const gain = audioContext.createGain();
      oscillator.type = 'sine';
      oscillator.frequency.value = frequency;
      gain.gain.setValueAtTime(0.06, audioContext.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration);
      oscillator.connect(gain);
      gain.connect(audioContext.destination);
      oscillator.start();
      oscillator.stop(audioContext.currentTime + duration);
    } catch (_) { /* Sound is optional; drawing still works. */ }
  }

  function render() {
    remainingLabel.textContent = `남은 학생 ${pool.length}명`;
    drawButton.disabled = busy || pool.length === 0;
    history.replaceChildren();
    if (!picked.length) {
      const item = document.createElement('li');
      item.textContent = '아직 뽑지 않았어요';
      history.append(item);
    }
    picked.forEach((name, index) => {
      const item = document.createElement('li');
      item.textContent = `${index + 1}. ${name}`;
      history.append(item);
    });
  }

  document.getElementById('apply').addEventListener('click', () => {
    if (busy) return;
    const next = namesInput.value.split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
    if (!next.length) { status.textContent = '이름을 하나 이상 입력해 주세요.'; return; }
    if (new Set(next).size !== next.length) { status.textContent = '같은 이름이 두 번 있어요. 명단을 확인해 주세요.'; return; }
    names = next;
    pool = names.slice();
    picked = [];
    result.textContent = '누가 나올까요?';
    status.textContent = `${names.length}명 명단을 적용했어요.`;
    render();
  });

  document.getElementById('reset').addEventListener('click', () => {
    if (busy) return;
    pool = names.slice();
    picked = [];
    result.textContent = '누가 나올까요?';
    status.textContent = '처음부터 다시 뽑을 수 있어요.';
    render();
  });

  drawButton.addEventListener('click', () => {
    if (busy || !pool.length) return;
    busy = true;
    status.textContent = '';
    stage.classList.add('mixing');
    playTone(440, .12);
    render();
    setTimeout(() => {
      const index = Math.floor(Math.random() * pool.length);
      const name = pool.splice(index, 1)[0];
      picked.push(name);
      result.textContent = name;
      stage.classList.remove('mixing');
      busy = false;
      playTone(660, .2);
      if (!pool.length) status.textContent = '모두 뽑았어요. 다시 시작을 눌러 주세요.';
      render();
    }, reducedMotion.matches ? 0 : 750);
  });

  soundButton.addEventListener('click', () => {
    soundOn = !soundOn;
    soundButton.textContent = soundOn ? '소리 켜짐' : '소리 꺼짐';
    soundButton.setAttribute('aria-pressed', String(soundOn));
    if (soundOn) playTone(520, .08);
  });
  render();
})();
</script>
</body>
</html>