Files
music_bot_v2/page/README.md
tkrmagid 3406ba64c4 feat(page): 곡 검색 결과를 리스트 행으로, 대기열 접기/펴기 추가 — 익숙한 형태로 정렬
"많이 쓰는 음악 사이트처럼" 이라는 요구에 맞춰, 형태가 관행과 달랐던 두 곳을 바꿨다.

1) 검색 결과: 그리드 카드 -> 리스트 행
   그리드(커버 중심)는 앨범·재생목록을 훑을 때 쓰는 형태이고, 개별 곡 검색 결과는
   스포티파이·유튜브뮤직 모두 행(번호/썸네일/제목·아티스트/재생시간) 형식이다.
   행 전체가 버튼이라 터치로도 바로 누를 수 있고, 마우스 환경에서는 번호 자리에
   재생 아이콘이 겹치는 스포티파이 곡 목록 방식을 따랐다. 좁은 화면은 hover 가 없어
   썸네일에 재생 아이콘을 항상 얹는다.

2) 대기열 접기/펴기
   재생바 우측(스포티파이와 같은 자리)에 대기열 버튼을 두어 넓은 화면에서도 접을 수 있게 했다.
   화면 크기별 기본값이 달라(좁은 화면=접힘, 넓은 화면=펼침) 상태를 둘로 나눴다.
   창 크기를 읽어 한 상태로 합치면 SSR/하이드레이션이 어긋나고 setState-in-effect 경고도 난다.

검증: tsc 통과, lint 에러 0. 데스크톱(1440x900)·모바일(390x844)에서 검색 결과 행 렌더링과
대기열 접기/펴기 동작을 실제 화면으로 확인. README 5-3 비교표에 두 항목 추가.
2026-09-29 14:11:06 +09:00

9.5 KiB

music_bot_v2 — 웹 대시보드(page/)

디스코드 음악봇(../bot)을 브라우저에서 제어하는 웹 대시보드. 디스코드 로그인 후 내가 속한 서버를 골라 검색·재생·대기열 관리·볼륨/탐색 등을 조작하고, 현재 재생 상태를 실시간으로 본다.

  • 스택: Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS 4 · NextAuth 4(Discord OAuth) · ioredis
  • 봇과의 통신: 직접 호출이 아니라 Redis Pub/Sub + Key로 봇과 주고받는다(아래 3장).
  • 리포 전체 개요는 ../README.md, 봇 본체는 ../bot/README.md 참고.

1. 빠른 시작 (개발)

cd page
npm install
# .env.local 작성 (2장 참고)
npm run dev        # http://localhost:3000

빌드/구동: npm run build → npm run start. 린트: npm run lint.

Redis(봇과 공유)와 디스코드 OAuth 앱이 있어야 실제 동작한다. Redis에 연결되지 않으면 부팅 시 에러를 던진다(src/lib/Redis.ts).


2. 환경변수 (.env.local)

변수 설명
REDIS_HOST / REDIS_PORT 봇과 공유하는 Redis. 봇 .env의 값과 동일해야 한다(미설정 시 부팅 에러).
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET 디스코드 OAuth 앱 자격증명(로그인·서버목록 scope).
NEXTAUTH_SECRET NextAuth 세션 서명 키.
NEXTAUTH_URL 배포 URL(예: https://music.example.com). 개발은 http://localhost:3000.
  • 로그인 scope는 identify email guilds(유저 정보 + 서버 목록). src/app/api/auth/[...nextauth]/route.ts.
  • 디스코드 개발자 포털의 OAuth2 Redirect에 <NEXTAUTH_URL>/api/auth/callback/discord를 등록해야 한다.

3. 봇과의 통신 구조 (Redis)

대시보드는 봇을 직접 호출하지 않고 Redis를 메시지 버스로 쓴다. 봇 쪽 구현은 ../bot/src/classes/RedisClient.ts.

3-1. 명령 (site → bot, 요청/응답 RPC) — src/lib/api.ts의 botRpc

site: PUBLISH "site-bot"  {action, serverId, userId, requestId, …}
bot : 처리 후  SETEX "<action-key>:<requestId>"  (예: player:play:<reqId>)  에 결과 저장
site: 그 키를 짧게 폴링(100ms→최대400ms 백오프, 기본 3초 타임아웃)해 결과 수신
  • requestId는 CSPRNG(randomUUID). 결과 키는 읽은 즉시 삭제 + TTL 보험.
  • action: search, player_play, player_playlist, player_now, player_pause, player_skip, player_seek, player_volume, queue_list, queue_set, queue_remove.

3-2. 실시간 상태 (bot → site, SSE) — src/lib/sse.ts의 botEventStream

bot : PUBLISH "bot-site"  {event:"player_update"|"queue_update", guildId}
site: "bot-site" 구독 → serverId 일치하는 것만 EventSource(SSE)로 브라우저에 전달(30초 keep-alive)
  • 브라우저는 /api/player/events?serverId=…, /api/queue/events?serverId=…(SSE)를 구독해 상태 변경 알림을 받고 now/queue/list를 다시 당겨온다.

4. API 라우트 (src/app/api)

모든 라우트는 세션 가드(requireSession)를 거치고 { success, … } 형태로 응답한다.

  • player: play, pause, skip, seek, volume, playlist, now, events(SSE)
  • queue: list, set, remove, events(SSE)
  • servers: 로그인 유저의 디스코드 서버 목록(봇이 있는 서버로 필터링)
  • search: 검색(봇의 search action → Spotify/YT Music/YT Video 결과)
  • auth: [...nextauth](Discord OAuth)

공용 헬퍼(src/lib/api.ts): requireSession, botRpc, readJsonBody, requireString/Number/Boolean, errorResponse.


5. UI 구성 (src/)

  • app/layout.tsx, app/page.tsx — 루트 레이아웃/페이지. components/Providers.tsx(세션 등), ToastProvider.tsx.
  • components/layout/ — TopNav(상단/서버선택·로그인), LeftSidebar(탐색).
  • components/player/ — PlayerBar(재생바/볼륨/탐색), MainContent(검색·결과), QueueSidebar(대기열).
  • types/music.ts — DiscordServer, Track/TrackInfo, SearchTrack, SearchResults 등 공용 도메인 타입.
  • next.config.ts — 보안 헤더(CSP: self + discord/spotify/ytimg/google 이미지 CDN만 허용, frame-ancestors 'none' 등).

5-1. 화면(모드)별 사용 흐름

화면은 라우트가 아니라 app/page.tsx의 ViewMode 상태로 전환된다(SPA 단일 페이지).

화면 진입 방법 화면에 보이는 것 여기서 할 수 있는 일
로그인 전 첫 방문 중앙 로그인 카드 디스코드 로그인. 사이드바/대기열/재생바는 렌더링하지 않는다
SERVER_LIST 로그인 직후, 홈 버튼, "다른 서버 선택" 봇이 있는 내 서버 카드 목록 서버 선택. 서버를 고르기 전에는 검색·재생이 잠겨 있다
SERVER_DETAIL 서버 카드 클릭 서버 이름/아이콘, 사용 안내 배너, 서버 ID·내 권한 검색 시작점. 좌측에 조작 중인 서버 표시
SEARCH_RESULT 검색창에 입력 후 Enter Spotify / YouTube Music / YouTube 영상 3개 구역의 곡 카드 곡을 눌러 대기열에 추가
대기열 패널 (넓은 화면) 항상 우측 고정 · (좁은 화면) 우하단 초록 버튼·재생바 목록 버튼 "지금 재생 중" + "다음 재생 목록" 순서 변경, 삭제, 특정 곡부터 재생

5-2. 버튼·조작 동작 정리

위치 요소 동작 비활성 조건
상단 홈 서버를 고른 상태면 서버 상세로, 아니면 서버 목록으로. 검색어 초기화 로그아웃 상태에선 숨김
상단 검색창 Enter 로 검색 실행 → SEARCH_RESULT 로그아웃·서버 미선택 시 잠금(안내 문구가 상황별로 바뀜)
상단 프로필/로그아웃 세션 종료 —
본문 서버 카드 서버 선택 → 서버 상세 —
본문 곡 카드(카드 전체가 버튼) POST /api/player/play → 대기열 추가. 성공/실패를 토스트로 알림 서버 미선택·미로그인
재생바 재생/일시정지 POST /api/player/pause (낙관적 UI, 실패 시 롤백) 봇 세션 없음/재생 중 아님
재생바 다음 곡 POST /api/player/skip 〃
재생바 이전 곡 미지원(봇 API 에 없음). 이유를 툴팁으로 표시하고 비활성 항상
재생바 진행바 드래그/클릭으로 탐색 → POST /api/player/seek. 좁은 화면에서는 재생바 상단에 전체 폭으로 배치 〃
재생바 스피커 아이콘 음소거/해제 토글(해제 시 직전 볼륨 복원) 〃
재생바 볼륨 슬라이더 드래그 중에는 화면만, 손을 뗄 때 POST /api/player/volume 1회 전송 〃
재생바 목록 아이콘 대기열 패널 열기/닫기(좁은 화면 전용) —
대기열 곡(제목 영역) 그 곡부터 재생. 앞선 곡들은 대기열에서 빠진다 첫 곡은 이미 다음 곡이라 동작 없음
대기열 ▲ ▼ 한 칸씩 순서 이동 → POST /api/queue/set (좁은 화면 전용, 드래그 대체) 맨 위/맨 아래
대기열 휴지통 해당 곡 제거 → POST /api/queue/remove (encoded 동봉해 엉뚱한 곡 삭제 방지) —
대기열 드래그 핸들 드래그로 순서 변경(넓은 화면 전용) —
전역 키보드 Space 재생/일시정지, M 음소거, ←/→ 5초 탐색, Ctrl/⌘+→ 다음 곡 입력창 포커스 중에는 가로채지 않음

5-3. 다른 음악 서비스와 비교해 맞춘 점

스포티파이 웹 플레이어 / 유튜브 뮤직 / 애플 뮤직에서 공통으로 쓰이는 관행을 기준으로 삼았다.

관행 예전 현재
곡 검색 결과 표시 그리드 카드 리스트 행(번호·썸네일·제목/아티스트·재생시간). 그리드는 앨범/재생목록처럼 커버가 주인공일 때 쓰는 형태이고, 개별 곡은 두 서비스 모두 행 형식이다
대기열 접기/펴기 넓은 화면에서 항상 고정 재생바 우측 대기열 버튼으로 접고 펼 수 있음(스포티파이와 같은 위치)
좁은 화면 대응 3열 고정이라 모바일에서 본문이 눌려 사용 불가 단일 컬럼 + 대기열 슬라이드 패널
대기열 상단에 "지금 재생 중" 없음(다음 곡만) 현재 곡을 강조해 표시
대기열 곡 클릭 → 그 곡부터 재생 없음 지원(앞 곡들은 빠짐 — 스포티파이 대기열과 동일)
스피커 아이콘 클릭 = 음소거 장식 아이콘 토글 버튼
키보드 단축키(Space/M/화살표) 없음 지원
재생시간 표기 없음 검색 결과 카드에 표시
터치 조작 크기(44px 내외) 재생 32px·삭제 24px·진행바 1px 44~48px, 진행바 히트영역 확대
hover 없이도 조작 가능 삭제·재생 버튼이 hover 시에만 노출 상시 노출

미지원(봇 API 한계): 이전 곡, 반복 재생, 특정 위치로의 셔플 재생. 필요하면 봇 쪽 액션 추가가 선행돼야 한다.


6. 배포 메모

  • Next.js 16 standalone/next start로 구동. 봇과 같은 Redis(.7)에 접근 가능해야 한다.
  • NEXTAUTH_URL/디스코드 Redirect URI를 실제 도메인으로 맞춘다.
  • 이 디렉터리는 Next가 제공하는 에이전트 규칙 파일(AGENTS.md, CLAUDE.md)을 둔다 — 코드 수정 전 참고.