바이브 코딩 #3_LP플레이어 (2)

김정언·2026년 5월 31일

바이브코딩

목록 보기
4/4

왜 안되는 걸까~를 찾아보니, 퍼가기 허용이 안됐을 경우 외부에서 재생이 안된다는 것을 알게 되었다. 그럼 다른 음악 사이트를 활용해보자!

  1. SoundCloud
  • 대부분 외부 임베드 허용
  • 공식 Widget API 제공 → LP 플레이어에 연동하기 쉬움
  • 인디 음악, 믹스셋 풍부
  1. 직접 오디오 파일 업로드
  • MP3/WAV 파일을 직접 서버나 클라우드(S3, Cloudflare R2)에 올려서 재생
  • 가장 안정적이고 자유로운 방법
  • <audio> 태그로 완전한 커스텀 UI 구현 가능
  1. Spotify Embed API
  • 공식 임베드 위젯 제공
  • 단, 무료 계정은 셔플 재생만 / 프리미엄은 전곡 재생
  • 디자인 커스터마이징이 제한적

나는 이 중에서 스포티파이를 선택했다! 이유는 공식 음원을 트는 것이 내 목적이었기 때문이다.


스포티파이 개발자 문서를 보면 음원 임베드할 수 있도록 API를 공개해놨다. 나는 이걸 활용할 것이다.

문서를 보면 oEmbed API와 iFrame API로 두 개가 나눠져있는데, 두 개의 차이를 알아보자.

oEmbed API vs iFrame API

oEmbed API — "링크 미리보기" 용

oEmbed는 Spotify URL에 대한 기본 정보와 embed 코드를 가져오는 API로, 주로 링크 미리보기(link unfurling) 기능에 사용돼요. spotify
동작 방식은 Spotify URL을 서버에 넘기면 JSON으로 아래 정보를 돌려줘요:

  • 콘텐츠 제목
  • 썸네일 이미지 URL
  • 바로 붙여넣을 수 있는 <iframe> HTML 코드

즉, "이 Spotify 링크 어떤 내용이야?" 를 물어보는 API예요. 유저가 링크를 붙여넣었을 때 자동으로 미리보기 카드를 만들어주는 SNS나 메신저 같은 서비스에 적합해요.

iFrame API — "플레이어 제어" 용

iFrame API는 웹앱에서 동적으로 Spotify 팟캐스트 에피소드를 불러오고, 재생하고, 일시정지할 수 있게 해주는 API예요. spotify JS로 플레이어를 직접 조작할 수 있어서 이런 메서드들을 제공해요:

  • play() / pause() / resume() / togglePlay()
  • seek(seconds) — 원하는 위치로 이동
  • loadUri() — 다른 트랙으로 교체
  • destroy() — 플레이어 제거

그리고 이벤트도 받을 수 있어요:

  • playback_started — 재생 시작됐을 때
  • playback_update — 재생 상태 변화 (현재 위치, 일시정지 여부 등)
  • ready — 플레이어 초기화 완료

그럼 우리 LP 플레이의 기능을 살펴보면 썸네일 이미지도 필요하고, 이름도 필요하고 음원도 실행해야하니, 둘다 활용해야된다.


LP 디자인이 좀 구린거 같아서, 카세트 테이프로 변경했다.

음 스포티파이 임베드 해오면 실행버튼이랑 연결이 될 줄 알았는데, 그게 아니라 자기네 디자인을 꼭 넣어야하는 거였다. 유튜브도 마찮가지. 그래서 임베드가 아니라 음원 API를 가져와야하는 걸 알고 Soundcloud로 변경하려고 한다.

음! 이제 버튼과 잘 연동되었다!


<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cassette Player</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  --blue:#1a3a6e;--silver:#c8cdd6;--silver-d:#9aa0ac;--silver-l:#e8ecf2;
  --dark:#1a1c20;--tape:#4a2810;
}
html,body{
  min-height:100vh;background:#1e1e22;
  display:flex;align-items:center;justify-content:center;
  font-family:'Share Tech Mono',monospace;padding:2rem 1rem;
}

/* ── Walkman body ── */
.wk{
  width:340px;background:var(--blue);border-radius:12px 12px 22px 22px;
  position:relative;user-select:none;
  box-shadow:4px 4px 0 var(--silver-d),8px 8px 0 rgba(0,0,0,.35),
    inset 1px 1px 0 rgba(255,255,255,.15),inset -1px -1px 0 rgba(0,0,0,.3);
}
.wk::after{
  content:'';position:absolute;right:-10px;top:8px;bottom:8px;width:10px;
  background:linear-gradient(to right,var(--silver-d),var(--silver));
  border-radius:0 6px 6px 0;box-shadow:2px 0 4px rgba(0,0,0,.3);
}
.screw{position:absolute;width:9px;height:9px;border-radius:50%;z-index:10;
  background:radial-gradient(circle at 35% 35%,#aab0bc,#6a7080);border:1px solid #555;}
.screw::after{content:'';position:absolute;top:50%;left:50%;
  transform:translate(-50%,-50%) rotate(45deg);width:5px;height:1px;background:rgba(0,0,0,.5);}
.s-tl{top:6px;left:6px}.s-tr{top:6px;right:6px}.s-bl{bottom:6px;left:6px}.s-br{bottom:6px;right:6px}

/* top bar */
.topbar{
  background:var(--silver);border-radius:10px 10px 0 0;padding:8px 14px;
  display:flex;align-items:center;justify-content:space-between;
  border-bottom:2px solid var(--silver-d);
}
.logo{font-family:'Orbitron',sans-serif;font-size:16px;font-weight:900;color:var(--dark);letter-spacing:3px;}
.top-r{display:flex;align-items:center;gap:10px;}
.jg{display:flex;flex-direction:column;align-items:center;gap:2px;}
.jlbl{font-size:7px;color:#555;letter-spacing:1px;}
.jacks{display:flex;gap:6px;}
.jack{width:8px;height:8px;border-radius:50%;background:#333;border:1.5px solid #555;box-shadow:inset 0 1px 2px rgba(0,0,0,.8);}
.led{width:8px;height:8px;border-radius:50%;background:#333;border:1px solid #555;transition:background .2s,box-shadow .2s;}
.led.on{background:#00ee44;box-shadow:0 0 7px #00ee44;}

/* cassette window */
.win-frame{
  margin:10px 14px;background:#0d1420;border-radius:6px;padding:10px;
  border:2px solid #060a10;position:relative;overflow:hidden;
  box-shadow:inset 0 2px 8px rgba(0,0,0,.8),inset 0 -1px 2px rgba(255,255,255,.04);
}
.win-frame::before{
  content:'';position:absolute;top:4px;left:6px;right:60%;height:32%;
  background:linear-gradient(135deg,rgba(255,255,255,.07) 0%,transparent 100%);
  border-radius:3px;pointer-events:none;z-index:20;
}

/* cassette */
.cass{width:100%;aspect-ratio:2.4;background:#f0e8d8;border-radius:5px;position:relative;overflow:hidden;border:1px solid #ddd;}
.clabel{position:absolute;inset:0;overflow:hidden;display:flex;align-items:center;justify-content:center;}
.clabel img{width:100%;height:100%;object-fit:cover;display:none;filter:brightness(.82) saturate(1.1);}
.clabel-def{
  width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
  background:linear-gradient(135deg,#0f0f18,#1a1a2e 50%,#0d1a3a);
}
.cld-brand{font-family:'Orbitron',sans-serif;font-size:7px;font-weight:700;color:rgba(255,255,255,.4);letter-spacing:3px;}
.cld-title{font-size:10px;color:rgba(255,255,255,.7);letter-spacing:1px;}
.side-a{position:absolute;top:5px;left:8px;font-family:'Orbitron',sans-serif;font-size:7px;font-weight:700;color:rgba(255,255,255,.55);letter-spacing:1px;z-index:6;}
.stereo-b{position:absolute;bottom:5px;right:8px;font-size:7px;color:rgba(255,255,255,.4);letter-spacing:2px;z-index:6;}

/* tape window */
.tape-win{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:54%;height:72%;background:rgba(4,7,14,.94);border-radius:3px;
  display:flex;align-items:center;justify-content:space-around;padding:0 8px;z-index:5;
}
.reel{width:38px;height:38px;position:relative;display:flex;align-items:center;justify-content:center;}
.rhub{
  width:100%;height:100%;border-radius:50%;position:relative;
  display:flex;align-items:center;justify-content:center;
  background:radial-gradient(circle at 35% 35%,#4a4a52,#1a1a1e);border:1.5px solid #555;
}
.wound{position:absolute;inset:0;border-radius:50%;border-style:solid;border-color:rgba(70,33,8,.92);transition:border-width .08s linear;}
.spoke{position:absolute;width:2px;height:13px;background:#666;top:50%;left:50%;transform-origin:50% 100%;border-radius:1px;}
.rc{width:10px;height:10px;border-radius:50%;background:radial-gradient(circle at 35% 35%,#888,#333);border:1px solid #777;z-index:2;}
.tape-strip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:28%;height:7px;background:var(--tape);z-index:3;}
.c-holes{position:absolute;bottom:4px;left:50%;transform:translateX(-50%);display:flex;gap:16px;z-index:6;}
.c-hole{width:5px;height:5px;border-radius:50%;background:rgba(0,0,0,.6);border:1px solid rgba(0,0,0,.4);}

/* labels */
.body-lbl{font-family:'Orbitron',sans-serif;font-size:10px;font-weight:700;color:rgba(255,255,255,.3);letter-spacing:4px;text-align:center;margin:4px 0 0;}
.tinfo{padding:8px 14px 2px;}
.tname{font-family:'Orbitron',sans-serif;font-size:11px;font-weight:700;color:rgba(255,255,255,.9);letter-spacing:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tsub{font-size:9px;color:rgba(255,255,255,.4);letter-spacing:1px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}

/* progress */
.prog-wrap{padding:6px 14px 0;}
.prog-bg{width:100%;height:3px;background:rgba(255,255,255,.12);border-radius:2px;cursor:pointer;position:relative;}
.prog-fill{height:100%;background:rgba(255,255,255,.6);border-radius:2px;width:0%;}
.time-row{display:flex;justify-content:space-between;margin-top:3px;}
.ttxt{font-size:8px;color:rgba(255,255,255,.3);}

/* volume */
.vol-row{padding:6px 14px 0;display:flex;align-items:center;gap:8px;}
.vlbl{font-size:8px;color:rgba(255,255,255,.35);letter-spacing:2px;width:28px;}
.vslider{flex:1;height:3px;-webkit-appearance:none;appearance:none;background:rgba(255,255,255,.14);border-radius:2px;outline:none;cursor:pointer;}
.vslider::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:2px;background:var(--silver);border:1px solid var(--silver-d);cursor:pointer;box-shadow:1px 1px 3px rgba(0,0,0,.5);}

/* transport */
.transport{padding:8px 14px 14px;display:flex;gap:5px;align-items:flex-end;justify-content:center;}
.btn{display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;}
.bbody{
  border-radius:4px;display:flex;align-items:center;justify-content:center;
  border:none;cursor:pointer;position:relative;overflow:hidden;
  transition:transform .08s,box-shadow .08s;
}
.bbody::after{content:'';position:absolute;top:0;left:0;right:0;height:40%;background:rgba(255,255,255,.12);border-radius:4px 4px 0 0;pointer-events:none;}
.bbody:active,.bbody.pressed{transform:translateY(2px)!important;box-shadow:none!important;}
.btn-gray .bbody{width:50px;height:27px;background:linear-gradient(to bottom,#6a7080,#4a5060);box-shadow:0 3px 0 #2a2f38,0 4px 6px rgba(0,0,0,.4);color:rgba(255,255,255,.85);font-size:14px;}
.btn-play .bbody{width:50px;height:27px;background:linear-gradient(to bottom,#6a7080,#4a5060);box-shadow:0 3px 0 #2a2f38,0 4px 6px rgba(0,0,0,.4);color:rgba(255,255,255,.9);font-size:14px;}
.btn-stop .bbody{width:50px;height:27px;background:linear-gradient(to bottom,#e03020,#a02010);box-shadow:0 3px 0 #601008,0 4px 6px rgba(0,0,0,.4);color:#fff;font-size:14px;}
.blbl{font-size:7px;color:rgba(255,255,255,.35);letter-spacing:1px;text-transform:uppercase;}

/* hidden SC iframe */
#sc-if{display:none;width:0;height:0;position:absolute;}

/* url input */
.input-sec{background:rgba(0,0,0,.25);border-top:1px solid rgba(0,0,0,.4);border-radius:0 0 20px 20px;padding:10px 14px 14px;display:flex;flex-direction:column;gap:6px;}
.irow{display:flex;gap:6px;}
.uin{flex:1;height:32px;padding:0 10px;font-family:'Share Tech Mono',monospace;font-size:11px;border-radius:4px;border:1px solid rgba(255,255,255,.13);background:rgba(0,0,0,.4);color:rgba(255,255,255,.85);outline:none;transition:border-color .2s;}
.uin:focus{border-color:rgba(255,255,255,.38);}
.uin::placeholder{color:rgba(255,255,255,.22);font-size:10px;}
.lbtn{height:32px;padding:0 12px;border-radius:4px;border:none;background:linear-gradient(to bottom,#e8720a,#b05008);color:#fff;font-family:'Share Tech Mono',monospace;font-size:11px;font-weight:700;cursor:pointer;box-shadow:0 2px 0 #702800;white-space:nowrap;transition:transform .08s;}
.lbtn:active{transform:translateY(1px);box-shadow:none;}
.hint{font-size:9px;color:rgba(255,255,255,.2);text-align:center;letter-spacing:.5px;line-height:1.6;}
</style>
</head>
<body>

<iframe id="sc-if" allow="autoplay; encrypted-media; fullscreen; clipboard-write"></iframe>
<script src="https://w.soundcloud.com/player/api.js"></script>

<div class="wk">
  <div class="screw s-tl"></div><div class="screw s-tr"></div>
  <div class="screw s-bl"></div><div class="screw s-br"></div>

  <div class="topbar">
    <span class="logo">MUSIC</span>
    <div class="top-r">
      <div class="jg">
        <span class="jlbl">HEADPHONES</span>
        <div class="jacks"><div class="jack"></div><div class="jack"></div></div>
      </div>
      <div class="led" id="led"></div>
    </div>
  </div>

  <div class="win-frame">
    <div class="cass">
      <div class="clabel">
        <div class="clabel-def" id="cLblDef">
          <span class="cld-brand">SOUNDCLOUD</span>
          <span class="cld-title">INSERT TAPE</span>
        </div>
        <img id="art" src="" alt="">
      </div>
      <div class="side-a">SIDE A</div>
      <div class="stereo-b">STEREO</div>

      <div class="tape-win">
        <div class="reel">
          <div class="rhub" id="reelL">
            <div class="wound" id="wL" style="border-width:9px"></div>
            <div class="spoke" style="transform:translate(-50%,-100%) rotate(0deg)"></div>
            <div class="spoke" style="transform:translate(-50%,-100%) rotate(120deg)"></div>
            <div class="spoke" style="transform:translate(-50%,-100%) rotate(240deg)"></div>
            <div class="rc"></div>
          </div>
        </div>
        <div class="tape-strip"></div>
        <div class="reel">
          <div class="rhub" id="reelR">
            <div class="wound" id="wR" style="border-width:0.5px"></div>
            <div class="spoke" style="transform:translate(-50%,-100%) rotate(60deg)"></div>
            <div class="spoke" style="transform:translate(-50%,-100%) rotate(180deg)"></div>
            <div class="spoke" style="transform:translate(-50%,-100%) rotate(300deg)"></div>
            <div class="rc"></div>
          </div>
        </div>
      </div>

      <div class="c-holes">
        <div class="c-hole"></div><div class="c-hole"></div><div class="c-hole"></div>
        <div class="c-hole"></div><div class="c-hole"></div>
      </div>
    </div>
  </div>

  <div class="body-lbl">STEREO CASSETTE PLAYER</div>

  <div class="tinfo">
    <div class="tname" id="tname">— — —</div>
    <div class="tsub"  id="tsub">SoundCloud URL을 입력하세요</div>
  </div>

  <div class="prog-wrap">
    <div class="prog-bg" id="progBg"><div class="prog-fill" id="progFill"></div></div>
    <div class="time-row">
      <span class="ttxt" id="tCur">0:00</span>
      <span class="ttxt" id="tDur">0:00</span>
    </div>
  </div>

  <div class="vol-row">
    <span class="vlbl">VOL</span>
    <input type="range" class="vslider" id="vol" min="0" max="100" value="80" step="1">
  </div>

  <div class="transport">
    <div class="btn btn-gray">
      <button class="bbody" id="bRew" aria-label="되감기"></button>
      <span class="blbl">REW</span>
    </div>
    <div class="btn btn-play">
      <button class="bbody" id="bPlay" aria-label="재생"></button>
      <span class="blbl" id="pLbl">PLAY</span>
    </div>
    <div class="btn btn-stop">
      <button class="bbody" id="bStop" aria-label="정지"></button>
      <span class="blbl">STOP</span>
    </div>
    <div class="btn btn-gray">
      <button class="bbody" id="bFF" aria-label="빨리감기"></button>
      <span class="blbl">FF</span>
    </div>
  </div>

  <div class="input-sec">
    <div class="irow">
      <input class="uin" id="urlIn" type="text" placeholder="https://soundcloud.com/artist/track">
      <button class="lbtn" id="loadBtn">LOAD</button>
    </div>
    <p class="hint">TRACK · PLAYLIST · ALBUM &nbsp;|&nbsp; SoundCloud URL을 붙여넣으세요</p>
  </div>
</div>

<script>
const $ = id => document.getElementById(id);
const reelL=$('reelL'), reelR=$('reelR'), wL=$('wL'), wR=$('wR');
const led=$('led'), bPlay=$('bPlay'), pLbl=$('pLbl');
const progFill=$('progFill'), progBg=$('progBg');
const tCur=$('tCur'), tDur=$('tDur');
const tname=$('tname'), tsub=$('tsub');
const art=$('art'), cLblDef=$('cLblDef');
const vol=$('vol');

const MAX_B=9, MIN_B=0.5, TOTAL=100;
let tapePos=0, angL=0, angR=0, rafId=null, lastT=null;
let mode='stop', dur=0, pos=0;
let widget=null, ready=false;

function fmt(ms){ const s=Math.floor(ms/1000); return Math.floor(s/60)+':'+String(s%60).padStart(2,'0'); }
function lerp(a,b,t){ return a+(b-a)*t; }

function updateWound(){
  const r=tapePos/TOTAL;
  wL.style.borderWidth=lerp(MAX_B,MIN_B,r).toFixed(1)+'px';
  wR.style.borderWidth=lerp(MIN_B,MAX_B,r).toFixed(1)+'px';
}

function animate(ts){
  if(!lastT) lastT=ts;
  const dt=Math.min((ts-lastT)/1000,0.05); lastT=ts;
  const spd=mode==='play'?1:8;

  if(mode==='play'){
    angL-=spd*dt*180; angR+=spd*dt*180;
    if(dur>0) tapePos=Math.min(TOTAL,Math.max(0,pos/dur*TOTAL));
  } else if(mode==='ff'){
    tapePos=Math.min(TOTAL,tapePos+spd*dt*8);
    angL-=spd*dt*900; angR+=spd*dt*900;
    if(tapePos>=TOTAL){setMode('stop');return;}
  } else if(mode==='rew'){
    tapePos=Math.max(0,tapePos-spd*dt*8);
    angL+=spd*dt*900; angR-=spd*dt*900;
    if(tapePos<=0){setMode('stop');return;}
  }

  reelL.style.transform=`rotate(${angL}deg)`;
  reelR.style.transform=`rotate(${angR}deg)`;
  updateWound();
  rafId=requestAnimationFrame(animate);
}

function setMode(m){
  mode=m;
  if(rafId){cancelAnimationFrame(rafId);rafId=null;}
  lastT=null;
  const playing=m==='play';
  led.classList.toggle('on',playing);
  bPlay.textContent=playing?'⏸':'▶';
  pLbl.textContent =playing?'PAUSE':'PLAY';
  if(m!=='stop'){lastT=null;rafId=requestAnimationFrame(animate);}
}

/* ── SC Widget ── */
function initWidget(src){
  $('sc-if').src=src;
  widget=SC.Widget($('sc-if'));
  ready=false;

  widget.bind(SC.Widget.Events.READY,()=>{
    ready=true;
    widget.setVolume(parseInt(vol.value));

    /* READY 시점에 곡 정보 즉시 로드 */
    function loadMeta(){
      widget.getCurrentSound(s=>{
        if(!s)return;
        tname.textContent=(s.title||'—').toUpperCase();
        tsub.textContent =(s.user?s.user.username:'').toUpperCase();
        const img=s.artwork_url||(s.user&&s.user.avatar_url)||'';
        if(img){
          art.src=img.replace('-large','-t500x500');
          art.style.display='block'; cLblDef.style.display='none';
        }
      });
      widget.getDuration(d=>{ if(d>0){ dur=d; tDur.textContent=fmt(d); } });
    }
    loadMeta();

    widget.bind(SC.Widget.Events.PLAY,()=>{
      setMode('play');
      loadMeta(); /* 플레이리스트 트랙 전환 시 갱신 */
    });

    widget.bind(SC.Widget.Events.PAUSE,()=>{ if(mode==='play') setMode('stop'); });
    widget.bind(SC.Widget.Events.FINISH,()=>{ setMode('stop'); tapePos=TOTAL; updateWound(); });

    widget.bind(SC.Widget.Events.PLAY_PROGRESS,data=>{
      pos=data.currentPosition;
      if(data.relativePosition>0) dur=pos/data.relativePosition;
      progFill.style.width=(data.relativePosition*100)+'%';
      tCur.textContent=fmt(pos);
    });
  });
}

/* ── Transport ── */
bPlay.addEventListener('click',()=>{
  if(!ready)return;
  if(mode==='play'){ widget.pause(); setMode('stop'); }
  else { widget.play(); setMode('play'); }
});
$('bStop').addEventListener('click',()=>{
  if(!ready)return;
  widget.pause(); setMode('stop');
});

/* FF / REW — hold */
const STEP=3000;
let ffIv=null, rewIv=null;

function doFF(){ if(!ready)return; widget.getPosition(p=>{ widget.getDuration(d=>widget.seekTo(Math.min(p+STEP,d-200))); }); }
function doRew(){ if(!ready)return; widget.getPosition(p=>widget.seekTo(Math.max(p-STEP,0))); }

function startFF(){ if(mode==='ff')return; setMode('ff'); $('bFF').classList.add('pressed'); doFF(); ffIv=setInterval(doFF,200); }
function endFF(){ clearInterval(ffIv); $('bFF').classList.remove('pressed'); if(mode==='ff') setMode(ready?'play':'stop'); }
function startRew(){ if(mode==='rew')return; setMode('rew'); $('bRew').classList.add('pressed'); doRew(); rewIv=setInterval(doRew,200); }
function endRew(){ clearInterval(rewIv); $('bRew').classList.remove('pressed'); if(mode==='rew') setMode(ready?'play':'stop'); }

$('bFF').addEventListener('mousedown',startFF);
$('bFF').addEventListener('touchstart',e=>{e.preventDefault();startFF();},{passive:false});
$('bFF').addEventListener('mouseup',endFF);
$('bFF').addEventListener('mouseleave',endFF);
$('bFF').addEventListener('touchend',endFF);
$('bRew').addEventListener('mousedown',startRew);
$('bRew').addEventListener('touchstart',e=>{e.preventDefault();startRew();},{passive:false});
$('bRew').addEventListener('mouseup',endRew);
$('bRew').addEventListener('mouseleave',endRew);
$('bRew').addEventListener('touchend',endRew);

/* volume */
vol.addEventListener('input',()=>{ if(ready) widget.setVolume(parseInt(vol.value)); });

/* progress click */
progBg.addEventListener('click',e=>{
  if(!ready||dur===0)return;
  const r=e.offsetX/progBg.offsetWidth;
  widget.getDuration(d=>widget.seekTo(Math.floor(r*d)));
});

/* load */
function load(){
  const url=$('urlIn').value.trim();
  if(!url||!url.includes('soundcloud.com')){tname.textContent='SOUNDCLOUD URL을 입력하세요';return;}
  setMode('stop');
  tapePos=0; angL=0; angR=0;
  reelL.style.transform=''; reelR.style.transform='';
  updateWound();
  ready=false; widget=null; dur=0; pos=0;
  progFill.style.width='0%'; tCur.textContent='0:00'; tDur.textContent='0:00';
  art.style.display='none'; cLblDef.style.display='flex';
  tname.textContent='LOADING...'; tsub.textContent='';
  const src=`https://w.soundcloud.com/player/?url=${encodeURIComponent(url)}&auto_play=false&show_artwork=true&enable_javascript_api=true`;
  initWidget(src);
}
$('loadBtn').addEventListener('click',load);
$('urlIn').addEventListener('keydown',e=>{ if(e.key==='Enter') load(); });

updateWound();
</script>
</body>
</html>

0개의 댓글