바이브코딩 #3_LP 플레이어

김정언·2026년 5월 30일

바이브코딩

목록 보기
3/4
post-thumbnail

아 오늘은 진짜 너무 지치는 하루다. 그냥 에쁘고 좋은 것만 보고 싶다. 고로.


LP 플레이어

목적) 노래를 듣고 싶은데, 유튜브 밤티로 듣기 싫다. LP 플레이어처럼 예쁘게 생긴 곳에서 유튜브 노래 듣고 싶다.

핵심기능)

  1. LP 플레이어 처럼 생긴 사이트
  2. 유튜브 URL를 붙여넣으면 그 썸네일을 가져와서 LP 커버로 보이고 노래가 플레이 된다.

형태) 웹사이트


<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LP Player</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=DM+Mono:wght@300;400&display=swap');

  :root {
    --bg: #f0ede8;
    --dark: #1a1814;
    --gold: #c9a84c;
    --orange: #e05a1e;
    --text: #2c2820;
    --muted: #8a7f74;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  body {
    background: var(--bg);
    font-family: 'DM Mono', monospace;
    color: var(--text);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 40px 20px;
    overflow-x: hidden;
  }

  body::before {
    content: '';
    position: fixed;
    inset: 0;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
    pointer-events: none;
    z-index: 100;
    opacity: 0.6;
  }

  .player-wrap {
    width: 100%;
    max-width: 420px;
    display: flex;
    flex-direction: column;
    gap: 28px;
  }

  .header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 0 4px;
  }
  .header-title {
    font-family: 'Playfair Display', serif;
    font-style: italic;
    font-size: 13px;
    color: var(--muted);
  }
  .header-vol { font-size: 10px; color: var(--muted); letter-spacing: 0.15em; }

  .turntable {
    background: linear-gradient(145deg, #eeeae3, #e0dbd2);
    border-radius: 24px;
    padding: 32px 28px 28px;
    box-shadow:
      8px 8px 24px rgba(0,0,0,0.12),
      -4px -4px 12px rgba(255,255,255,0.7),
      inset 0 1px 0 rgba(255,255,255,0.5);
    position: relative;
    overflow: hidden;
  }

  .turntable.playing-state {
    animation: pulse-ring 2s ease-out infinite;
  }

  @keyframes pulse-ring {
    0%   { box-shadow: 0 0 0 0   rgba(201,168,76,.35), 8px 8px 24px rgba(0,0,0,.12), -4px -4px 12px rgba(255,255,255,.7); }
    70%  { box-shadow: 0 0 0 12px rgba(201,168,76,0),  8px 8px 24px rgba(0,0,0,.12), -4px -4px 12px rgba(255,255,255,.7); }
    100% { box-shadow: 0 0 0 0   rgba(201,168,76,0),   8px 8px 24px rgba(0,0,0,.12), -4px -4px 12px rgba(255,255,255,.7); }
  }

  .turntable::before {
    content: '';
    position: absolute;
    top: 20px; left: 20px;
    width: 40px; height: 40px;
    background-image: radial-gradient(circle, #c8c4bb 1.5px, transparent 1.5px);
    background-size: 8px 8px;
    opacity: 0.7;
  }
  .turntable::after {
    content: '';
    position: absolute;
    top: 20px; right: 20px;
    width: 40px; height: 40px;
    background-image: radial-gradient(circle, #c8c4bb 1.5px, transparent 1.5px);
    background-size: 8px 8px;
    opacity: 0.7;
  }

  .disc-area {
    position: relative;
    width: 260px;
    height: 260px;
    margin: 0 auto 20px;
  }

  .platter {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 50%, #2a2620, #111);
    box-shadow: 0 0 0 3px #333, 0 0 0 4px #1a1814, 4px 8px 24px rgba(0,0,0,.4);
  }

  .platter::before {
    content: '';
    position: absolute;
    inset: 8px;
    border-radius: 50%;
    background: repeating-radial-gradient(
      circle at center,
      transparent 0, transparent 3px,
      rgba(255,255,255,0.025) 3px, rgba(255,255,255,0.025) 4px
    );
  }

  .platter.spinning { animation: spin 3s linear infinite; }
  @keyframes spin { to { transform: rotate(360deg); } }

  .cover-ring {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: 110px; height: 110px;
    border-radius: 50%;
    background: #111;
    border: 3px solid #333;
    overflow: hidden;
    z-index: 2;
  }

  /* Cover image fills the ring */
  .cover-ring img {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    border-radius: 50%;
    opacity: 0;
    transition: opacity 0.4s;
  }
  .cover-ring img.loaded { opacity: 1; }

  .cover-placeholder {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 40% 40%, #e05a1e, #c9a84c, #1a1814);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: opacity 0.4s;
  }

  .center-hole {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: 10px; height: 10px;
    border-radius: 50%;
    background: #f0ede8;
    z-index: 3;
    box-shadow: 0 0 0 1px rgba(0,0,0,.3);
  }

  /* Tonearm */
  .tonearm-wrap {
    position: absolute;
    top: -10px; right: -10px;
    width: 120px; height: 160px;
    z-index: 10;
    transform-origin: 90px 20px;
    transition: transform 0.8s cubic-bezier(.4,0,.2,1);
  }
  .tonearm-wrap.resting { transform: rotate(-25deg); }
  .tonearm-wrap.playing { transform: rotate(-8deg); }

  .tonearm-pivot {
    position: absolute;
    top: 10px; right: 18px;
    width: 24px; height: 24px;
    border-radius: 50%;
    background: linear-gradient(135deg, #d4d0c8, #a8a49c);
    box-shadow: 2px 2px 6px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.4);
  }
  .tonearm-pivot::after {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%,-50%);
    width: 8px; height: 8px;
    border-radius: 50%;
    background: linear-gradient(135deg, #e8e4dc, #c8c4bc);
  }

  .tonearm-shaft {
    position: absolute;
    top: 22px; right: 28px;
    width: 3px; height: 110px;
    background: linear-gradient(to bottom, #d4d0c8, #b8b4ac);
    transform-origin: top center;
    transform: rotate(15deg);
    border-radius: 2px;
  }

  .tonearm-head {
    position: absolute;
    bottom: 0; left: -8px;
    width: 18px; height: 14px;
    background: linear-gradient(135deg, #e8e4dc, #c8c4bc);
    border-radius: 3px;
    box-shadow: 2px 2px 4px rgba(0,0,0,.25);
  }

  .tonearm-needle {
    position: absolute;
    bottom: -6px; left: 6px;
    width: 2px; height: 8px;
    background: var(--gold);
    border-radius: 0 0 2px 2px;
  }

  .track-info { text-align: center; padding: 0 8px; }

  .track-title {
    font-family: 'Playfair Display', serif;
    font-size: 20px;
    font-weight: 700;
    color: var(--dark);
    margin-bottom: 4px;
    line-height: 1.3;
    min-height: 26px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .track-artist {
    font-size: 11px;
    color: var(--muted);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    min-height: 16px;
  }

  .progress-area { margin-top: 16px; }

  .progress-bar-wrap {
    position: relative;
    height: 3px;
    background: #d4d0c8;
    border-radius: 2px;
    cursor: pointer;
  }

  .progress-bar-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--gold), var(--orange));
    border-radius: 2px;
    width: 0%;
    transition: width 0.5s linear;
  }

  .time-row {
    display: flex;
    justify-content: space-between;
    margin-top: 6px;
    font-size: 9px;
    color: var(--muted);
    letter-spacing: 0.1em;
  }

  .controls {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 20px;
    margin-top: 20px;
  }

  .ctrl-btn {
    background: none; border: none;
    cursor: pointer;
    color: var(--muted);
    transition: color .2s, transform .1s;
    padding: 6px;
    display: flex; align-items: center; justify-content: center;
  }
  .ctrl-btn:hover { color: var(--dark); }
  .ctrl-btn:active { transform: scale(0.9); }

  .play-btn {
    width: 52px; height: 52px;
    border-radius: 50%;
    background: var(--dark);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; border: none;
    box-shadow: 4px 4px 12px rgba(0,0,0,.2), -2px -2px 8px rgba(255,255,255,.5);
    transition: transform .15s;
  }
  .play-btn:hover { transform: scale(1.05); }
  .play-btn:active { transform: scale(0.95); }
  .play-btn svg { color: #f0ede8; }

  .volume-row {
    display: flex; align-items: center;
    gap: 10px; margin-top: 16px; padding: 0 4px;
  }
  .volume-row svg { color: var(--muted); flex-shrink: 0; }

  .volume-slider {
    flex: 1;
    -webkit-appearance: none;
    height: 3px;
    background: #d4d0c8;
    border-radius: 2px;
    outline: none; cursor: pointer;
  }
  .volume-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 12px; height: 12px;
    border-radius: 50%;
    background: var(--dark);
    box-shadow: 0 1px 4px rgba(0,0,0,.3);
    cursor: pointer;
  }

  .url-card {
    background: linear-gradient(145deg, #eeeae3, #e2ddd6);
    border-radius: 16px;
    padding: 20px;
    box-shadow: 4px 4px 12px rgba(0,0,0,.08), -2px -2px 8px rgba(255,255,255,.6);
  }

  .url-label {
    font-size: 9px; letter-spacing: 0.18em;
    text-transform: uppercase; color: var(--muted); margin-bottom: 10px;
  }

  .url-row { display: flex; gap: 8px; }

  .url-input {
    flex: 1;
    background: rgba(255,255,255,.5);
    border: 1px solid rgba(0,0,0,.1);
    border-radius: 10px;
    padding: 10px 14px;
    font-family: 'DM Mono', monospace;
    font-size: 11px; color: var(--text);
    outline: none;
    transition: border-color .2s, background .2s;
  }
  .url-input::placeholder { color: #b8b4ac; }
  .url-input:focus { border-color: var(--gold); background: rgba(255,255,255,.7); }

  .url-submit {
    background: var(--dark); color: #f0ede8;
    border: none; border-radius: 10px;
    padding: 10px 16px;
    font-family: 'DM Mono', monospace;
    font-size: 11px; cursor: pointer;
    letter-spacing: 0.05em;
    transition: background .2s, transform .1s;
    white-space: nowrap;
  }
  .url-submit:hover { background: #2c2820; }
  .url-submit:active { transform: scale(0.97); }

  .status-msg {
    font-size: 10px; color: var(--muted);
    margin-top: 8px; letter-spacing: 0.08em;
    min-height: 14px; text-align: center;
  }
  .status-msg.error { color: var(--orange); }
  .status-msg.warn  { color: var(--gold); }

  /* YouTube iframe: tiny but functional (must be in DOM & visible to browser) */
  #yt-iframe-wrap {
    position: fixed;
    bottom: 2px; right: 2px;
    width: 2px; height: 2px;
    overflow: hidden;
  }

  .footer {
    display: flex; justify-content: space-between;
    padding: 0 4px;
    font-size: 9px; color: #b8b4ac; letter-spacing: 0.1em;
  }
</style>
</head>
<body>

<div class="player-wrap">
  <div class="header">
    <span class="header-title">LP Player</span>
    <span class="header-vol">VOL. 001</span>
  </div>

  <div class="turntable" id="turntable">
    <div class="tonearm-wrap resting" id="tonearm">
      <div class="tonearm-pivot"></div>
      <div class="tonearm-shaft">
        <div class="tonearm-head"><div class="tonearm-needle"></div></div>
      </div>
    </div>

    <div class="disc-area">
      <div class="platter" id="platter">
        <div class="cover-ring" id="cover-ring">
          <div class="cover-placeholder" id="cover-ph">
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.5">
              <path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>
            </svg>
          </div>
          <img id="cover-img" alt="" />
        </div>
        <div class="center-hole"></div>
      </div>
    </div>

    <div class="track-info">
      <div class="track-title" id="track-title"></div>
      <div class="track-artist" id="track-artist">URL을 입력하세요</div>
    </div>

    <div class="progress-area">
      <div class="progress-bar-wrap" id="progress-wrap">
        <div class="progress-bar-fill" id="progress-fill"></div>
      </div>
      <div class="time-row">
        <span id="time-current">0:00</span>
        <span id="time-total">0:00</span>
      </div>
    </div>

    <div class="controls">
      <button class="ctrl-btn" id="btn-prev">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
          <path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/>
        </svg>
      </button>
      <button class="play-btn" id="btn-play">
        <svg id="icon-play" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
        <svg id="icon-pause" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
      </button>
      <button class="ctrl-btn" id="btn-next">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
          <path d="M6 18l8.5-6L6 6v12zm2.5-6 8.5 6V6z"/><path d="M16 6h2v12h-2z"/>
        </svg>
      </button>
    </div>

    <div class="volume-row">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
        <path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5zm7-.17v6.34L9.83 13H7v-2h2.83L12 8.83z"/>
      </svg>
      <input type="range" class="volume-slider" id="vol-slider" min="0" max="100" value="80">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
        <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
      </svg>
    </div>
  </div>

  <div class="url-card">
    <div class="url-label">YouTube URL 입력</div>
    <div class="url-row">
      <input type="text" class="url-input" id="url-input" placeholder="https://youtube.com/watch?v=..." />
      <button class="url-submit" id="url-btn">▶ 재생</button>
    </div>
    <div class="status-msg" id="status"></div>
  </div>

  <div class="footer">
    <span>LP PLAYER</span>
    <span>◈ ANALOG VIBES</span>
  </div>
</div>

<div id="yt-iframe-wrap">
  <div id="yt-player"></div>
</div>

<script>
// ── Helpers ──────────────────────────────────────────────────────────

// Extracts YouTube video ID from any YouTube URL format
function extractId(url) {
  // Handles: watch?v=, youtu.be/, /embed/, /shorts/, /live/
  // Also strips extra params like &list= or ?si=
  const clean = url.trim();
  const patterns = [
    /[?&]v=([a-zA-Z0-9_-]{11})/,           // watch?v=ID
    /youtu\.be\/([a-zA-Z0-9_-]{11})/,       // youtu.be/ID
    /\/embed\/([a-zA-Z0-9_-]{11})/,          // /embed/ID
    /\/shorts\/([a-zA-Z0-9_-]{11})/,         // /shorts/ID
    /\/live\/([a-zA-Z0-9_-]{11})/,           // /live/ID
  ];
  for (const p of patterns) {
    const m = clean.match(p);
    if (m) return m[1];
  }
  return null;
}

function fmt(s) {
  const m = Math.floor(s / 60);
  return `${m}:${String(Math.floor(s % 60)).padStart(2,'0')}`;
}

function setStatus(msg, type) {
  const el = document.getElementById('status');
  el.textContent = msg;
  el.className = 'status-msg' + (type ? ` ${type}` : '');
}

// ── State ────────────────────────────────────────────────────────────
let player      = null;
let isPlaying   = false;
let hasVideo    = false;
let progressTmr = null;
let pendingId   = null;
let playerReady = false;

// ── DOM refs ─────────────────────────────────────────────────────────
const platter    = document.getElementById('platter');
const tonearm    = document.getElementById('tonearm');
const turntable  = document.getElementById('turntable');
const coverImg   = document.getElementById('cover-img');
const coverPH    = document.getElementById('cover-ph');
const trackTitle = document.getElementById('track-title');
const trackArtist= document.getElementById('track-artist');
const iconPlay   = document.getElementById('icon-play');
const iconPause  = document.getElementById('icon-pause');
const progFill   = document.getElementById('progress-fill');
const timeCur    = document.getElementById('time-current');
const timeTot    = document.getElementById('time-total');
const urlInput   = document.getElementById('url-input');
const volSlider  = document.getElementById('vol-slider');

// ── YouTube IFrame API ───────────────────────────────────────────────
const ytScript = document.createElement('script');
ytScript.src = 'https://www.youtube.com/iframe_api';
document.head.appendChild(ytScript);

window.onYouTubeIframeAPIReady = () => {
  player = new YT.Player('yt-player', {
    width: '2', height: '2',
    playerVars: {
      autoplay: 0,
      controls: 0,
      playsinline: 1,
      rel: 0,
      fs: 0
      // NOTE: no 'origin' — causes issues with file:// protocol
    },
    events: {
      onReady(e) {
        playerReady = true;
        e.target.setVolume(parseInt(volSlider.value));
        if (pendingId) {
          doLoad(pendingId);
          pendingId = null;
        }
      },
      onStateChange(e) {
        const S = YT.PlayerState;
        if (e.data === S.PLAYING) {
          setPlaying(true);
          startProgress();
          setStatus('');
        } else if (e.data === S.PAUSED) {
          setPlaying(false);
          stopProgress();
        } else if (e.data === S.ENDED) {
          setPlaying(false);
          stopProgress();
          progFill.style.width = '100%';
        } else if (e.data === S.BUFFERING) {
          setStatus('버퍼링 중...');
        }
      },
      onError(e) {
        const msgs = {
          2:   '잘못된 영상 ID입니다.',
          5:   'HTML5 재생 오류입니다.',
          100: '영상을 찾을 수 없습니다.',
          101: '이 영상은 외부 재생이 차단되어 있습니다.',
          150: '이 영상은 외부 재생이 차단되어 있습니다.',
        };
        setStatus(msgs[e.data] || `재생 오류 (${e.data})`, 'error');
        setPlaying(false);
      }
    }
  });
};

// ── Load video ───────────────────────────────────────────────────────
async function loadVideo(rawUrl) {
  const id = extractId(rawUrl);
  if (!id) {
    setStatus('올바른 YouTube URL을 입력해주세요.', 'error');
    return;
  }

  setStatus('로딩 중...');
  progFill.style.width = '0%';
  timeCur.textContent = '0:00';
  timeTot.textContent = '0:00';

  // ── 1. Thumbnail ──────────────────────────────────────────────────
  // Try maxresdefault → hqdefault → mqdefault in order
  loadThumb(id);

  // ── 2. Title & Artist via oEmbed ──────────────────────────────────
  try {
    const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${id}&format=json`;
    const r = await fetch(oembedUrl);
    if (r.ok) {
      const d = await r.json();
      trackTitle.textContent  = d.title       || id;
      trackArtist.textContent = d.author_name || 'YouTube';
    } else {
      trackTitle.textContent  = id;
      trackArtist.textContent = 'YouTube';
    }
  } catch {
    trackTitle.textContent  = id;
    trackArtist.textContent = 'YouTube';
  }

  // ── 3. Play ───────────────────────────────────────────────────────
  hasVideo = true;
  if (playerReady) {
    doLoad(id);
  } else {
    pendingId = id;
    setStatus('플레이어 초기화 중...');
  }
}

function loadThumb(id) {
  // YouTube thumbnail via noembed proxy to avoid CORS
  // Try multiple thumbnail qualities with fallback
  const urls = [
    `https://i.ytimg.com/vi/${id}/hqdefault.jpg`,
    `https://i.ytimg.com/vi/${id}/mqdefault.jpg`,
    `https://i.ytimg.com/vi/${id}/default.jpg`,
  ];

  coverImg.classList.remove('loaded');
  coverPH.style.opacity = '1';

  let idx = 0;
  function tryNext() {
    if (idx >= urls.length) return;
    coverImg.src = urls[idx];
    idx++;
  }

  coverImg.onload = function() {
    // YouTube returns a "no thumbnail" 120x90 image for missing qualities
    // Real thumbnails are larger; skip tiny placeholder images
    if (this.naturalWidth <= 120 && this.naturalHeight <= 90 && idx < urls.length) {
      tryNext();
      return;
    }
    coverImg.classList.add('loaded');
    coverPH.style.opacity = '0';
  };

  coverImg.onerror = function() {
    tryNext();
  };

  tryNext();
}

function doLoad(id) {
  player.loadVideoById({ videoId: id, suggestedQuality: 'small' });
  setTimeout(() => {
    try { player.playVideo(); } catch(e) {}
  }, 500);
}

// ── Playback state ───────────────────────────────────────────────────
function setPlaying(state) {
  isPlaying = state;
  platter.classList.toggle('spinning', state);
  tonearm.classList.toggle('resting', !state);
  tonearm.classList.toggle('playing', state);
  turntable.classList.toggle('playing-state', state);
  iconPlay.style.display  = state ? 'none' : '';
  iconPause.style.display = state ? ''     : 'none';
}

function startProgress() {
  stopProgress();
  progressTmr = setInterval(() => {
    if (!player || typeof player.getCurrentTime !== 'function') return;
    const cur = player.getCurrentTime() || 0;
    const dur = player.getDuration()    || 0;
    if (dur > 0) {
      progFill.style.width = (cur / dur * 100) + '%';
      timeCur.textContent  = fmt(cur);
      timeTot.textContent  = fmt(dur);
    }
  }, 500);
}

function stopProgress() { clearInterval(progressTmr); progressTmr = null; }

// ── Events ───────────────────────────────────────────────────────────
document.getElementById('url-btn').onclick = () => {
  const v = urlInput.value.trim();
  if (v) loadVideo(v);
};

urlInput.onkeydown = e => {
  if (e.key === 'Enter') { const v = urlInput.value.trim(); if (v) loadVideo(v); }
};

urlInput.addEventListener('paste', () => {
  setTimeout(() => {
    const v = urlInput.value.trim();
    if (v.includes('youtu')) loadVideo(v);
  }, 60);
});

document.getElementById('btn-play').onclick = () => {
  if (!hasVideo) { setStatus('먼저 YouTube URL을 입력하세요.', 'warn'); return; }
  if (!playerReady) return;
  isPlaying ? player.pauseVideo() : player.playVideo();
};

document.getElementById('btn-prev').onclick = () => {
  if (!playerReady || !hasVideo) return;
  player.seekTo(Math.max(0, (player.getCurrentTime() || 0) - 10), true);
};

document.getElementById('btn-next').onclick = () => {
  if (!playerReady || !hasVideo) return;
  const dur = player.getDuration() || 0;
  player.seekTo(Math.min(dur, (player.getCurrentTime() || 0) + 10), true);
};

volSlider.oninput = () => {
  if (playerReady) player.setVolume(parseInt(volSlider.value));
};

document.getElementById('progress-wrap').onclick = e => {
  if (!playerReady || !hasVideo) return;
  const rect  = e.currentTarget.getBoundingClientRect();
  const ratio = (e.clientX - rect.left) / rect.width;
  const dur   = player.getDuration() || 0;
  if (dur > 0) player.seekTo(ratio * dur, true);
};
</script>
</body>
</html>

큰 실패.. 왜 실패했지.. 내일 다시 디버깅 해야지

0개의 댓글