# 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`](../README.md), 봇 본체는 [`../bot/README.md`](../bot/README.md) 참고. --- ## 1. 빠른 시작 (개발) ```bash 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에 `/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 ":" (예: player:play:) 에 결과 저장 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`)을 둔다 — 코드 수정 전 참고.