- 상단 바: ☰ + 로고 + 반투명 검색창 + 프로필 메뉴(좁은 화면은 돋보기 → 전체 폭 검색) - 왼쪽 메뉴: 홈/서버 목록 + 봇이 있는 내 서버 목록, 넓은 화면 펼침(240)/접힘(72), 좁은 화면 서랍 - 홈: 서버를 원형 카드로, 서버 페이지: 앨범 페이지 형태(왼쪽 아이콘·정보, 오른쪽 재생 목록) - 검색: 필터 칩(전체/노래/동영상/Spotify) + 상위 검색결과 카드 + 구역별 목록(모두 표시) - 하단 재생바: 위쪽 빨간 진행선, 조작·시간 / 곡 정보 / hover 볼륨·재생 화면(⌃) - 재생 화면: 큰 앨범아트 + 다음 트랙 탭, 좁은 화면은 전체 화면 플레이어 - 재생/대기열/서버 로직을 hooks(usePlayer/useQueue/useServers)로 분리해 구독을 한 번만 연다 - 스낵바 알림, 파비콘, README 화면·버튼 문서 갱신 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
10 KiB
Markdown
148 lines
10 KiB
Markdown
# 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에 `<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/`)
|
|
|
|
디자인은 **유튜브 뮤직(웹) 다크 테마**를 그대로 따른다: 배경 `#030303`, 재생바/메뉴 `#212121`, 강조 빨강 `#ff0000`,
|
|
보조 글자는 흰색 70%/50% 투명도(색상 토큰은 `app/globals.css`의 `--color-yt-*`).
|
|
|
|
- `app/layout.tsx`, `app/page.tsx` — 루트 레이아웃/페이지(상단 바 · 왼쪽 메뉴 · 본문 · 하단 재생바 배치). `app/icon.svg` 파비콘.
|
|
- `components/layout/` — `TopNav`(☰·로고·검색·프로필 메뉴), `LeftSidebar`(왼쪽 메뉴: 홈/서버 목록 + 내 서버 목록).
|
|
- `components/player/` — `MainContent`(홈·서버 페이지·검색 결과), `PlayerBar`(하단 재생바), `PlayerPage`(펼친 재생 화면),
|
|
`QueueList`("다음 트랙" 목록).
|
|
- `components/ui.tsx` — 로고, 썸네일, 서버 아이콘, 로딩 표시, 재생 중 이퀄라이저.
|
|
- `components/ToastProvider.tsx` — 왼쪽 아래 스낵바 알림.
|
|
- `hooks/` — 데이터·조작 로직. 화면 여러 곳이 같은 상태를 쓰므로 `app/page.tsx`에서 한 번만 구독해 내려준다.
|
|
- `usePlayer` 현재 곡·진행·볼륨 + 재생/일시정지/다음/탐색/볼륨/단축키(SSE `player/events`)
|
|
- `useQueue` 대기열 + 순서 변경/삭제/특정 곡부터 재생(SSE `queue/events`)
|
|
- `useServers` 봇이 있는 내 서버 목록(세션 캐시)
|
|
- `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` (서버 페이지) | 서버 카드/왼쪽 메뉴의 서버, 로고·"홈" | 유튜브 뮤직 앨범 페이지 형태: 왼쪽 서버 아이콘·이름·권한·사용 안내, 오른쪽 "재생 목록"(지금 재생 중 + 다음 트랙) | 검색 시작, 대기열 조작 |
|
|
| `SEARCH_RESULT` | 검색창 Enter | 필터 칩(전체/노래/동영상/Spotify), 상위 검색결과 카드, 구역별 곡 목록(전체 탭은 5곡씩 + "모두 표시") | 곡을 눌러 대기열에 추가 |
|
|
| 재생 화면 | 재생바의 곡 정보·⌃ 버튼, 서버 페이지의 목록 아이콘 | 넓은 화면: 큰 앨범아트 + "다음 트랙" 탭 / 좁은 화면: 전체 화면 플레이어(진행바·조작·볼륨) + 다음 트랙 | 대기열 조작, (좁은 화면) 재생 조작 |
|
|
|
|
- 재생바는 유튜브 뮤직처럼 **재생 중인 곡이 있을 때만** 나타난다.
|
|
- 왼쪽 메뉴: 넓은 화면은 ☰ 로 펼침(240px)/접힘(아이콘만 72px), 좁은 화면은 ☰ 로 서랍을 연다.
|
|
- 좁은 화면 상단 바는 검색창 대신 돋보기 버튼 → 누르면 바 전체가 검색창이 된다.
|
|
|
|
### 5-2. 버튼·조작 동작 정리
|
|
|
|
| 위치 | 요소 | 동작 | 비활성 조건 |
|
|
|------|------|------|-------------|
|
|
| 상단 | ☰ | 왼쪽 메뉴 펼침/접힘(좁은 화면은 서랍) | — |
|
|
| 상단 | 로고 | 서버를 고른 상태면 서버 페이지로, 아니면 홈으로 | — |
|
|
| 상단 | 검색창 | Enter 로 검색 → `SEARCH_RESULT`. ✕ 로 검색어 지우기 | 로그아웃·서버 미선택 시 잠금(안내 문구가 상황별로 바뀜) |
|
|
| 상단 | 프로필 사진 | 계정 메뉴(이름 + 로그아웃). 바깥 클릭/Esc 로 닫힘 | — |
|
|
| 왼쪽 메뉴 | 홈 / 서버 목록 / 내 서버 | 서버 페이지 / 서버 고르기 / 해당 서버로 바로 전환 | — |
|
|
| 서버 페이지 | 노래 검색 | 상단 검색창에 포커스(좁은 화면은 검색 모드로 전환) | — |
|
|
| 서버 페이지 | 목록 아이콘 | 재생 화면 열기 | — |
|
|
| 검색 | 필터 칩 | 해당 출처 결과만 전부 보기 | 결과 0건인 칩 |
|
|
| 검색 | 곡 행(행 전체가 버튼) / 상위 검색결과 "재생" | `POST /api/player/play` → 대기열 추가(재생 중이 아니면 바로 재생). 스낵바로 결과 알림. 썸네일 위 재생 아이콘은 마우스면 hover 때, 터치 화면이면 항상 | 서버 미선택·미로그인 |
|
|
| 재생바 | 진행바(위쪽 가장자리 빨간 선) | hover 시 굵어지고 손잡이 표시. 드래그/클릭/키보드로 탐색 → `POST /api/player/seek` | 봇 세션 없음/재생 중 아님 |
|
|
| 재생바 | 재생/일시정지 · 다음 곡 | `POST /api/player/pause`(낙관적 UI, 실패 시 롤백) · `POST /api/player/skip` | 〃 |
|
|
| 재생바 | 이전 곡 | 미지원(봇 API 에 없음). 이유를 툴팁으로 표시하고 비활성 | 항상 |
|
|
| 재생바 | 곡 정보 / ⌃ | 재생 화면 열기·닫기 | — |
|
|
| 재생바 | 스피커 아이콘 | 클릭 = 음소거/해제(해제 시 직전 볼륨 복원). hover 시 왼쪽에 볼륨 슬라이더가 펼쳐짐 | 봇 세션 없음/재생 중 아님 |
|
|
| 재생바·재생 화면 | 볼륨 슬라이더 | 드래그 중에는 화면만, 손을 뗄 때 `POST /api/player/volume` 1회 전송 | 〃 |
|
|
| 다음 트랙 | 곡 | 그 곡부터 재생. 앞선 곡들은 대기열에서 빠진다(첫 곡이면 다음 곡으로 넘기기만) | — |
|
|
| 다음 트랙 | 손잡이 드래그 | 순서 변경(마우스 전용, hover 시 표시) | — |
|
|
| 다음 트랙 | ▲ ▼ | 한 칸씩 순서 이동 → `POST /api/queue/set` (터치 화면 전용, 드래그 대체) | 맨 위/맨 아래 |
|
|
| 다음 트랙 | 휴지통 | 해당 곡 제거 → `POST /api/queue/remove` (`encoded` 동봉해 엉뚱한 곡 삭제 방지). 마우스면 hover 때, 터치 화면이면 항상 표시 | — |
|
|
| 재생 화면 | ⌄ / Esc | 재생 화면 닫기 | — |
|
|
| 전역 | 키보드 | `Space` 재생/일시정지, `M` 음소거, `←`/`→` 5초 탐색, `Ctrl/⌘+→` 다음 곡 | 입력창 포커스 중에는 가로채지 않음 |
|
|
|
|
미지원(봇 API 한계): 이전 곡, 반복 재생, 셔플, 좋아요/가사. 유튜브 뮤직에 있는 이 버튼들은 동작하지 않는 가짜 버튼이 되므로 넣지 않았다.
|
|
|
|
---
|
|
|
|
## 6. 배포 메모
|
|
|
|
- Next.js 16 standalone/`next start`로 구동. 봇과 같은 Redis(.7)에 접근 가능해야 한다.
|
|
- `NEXTAUTH_URL`/디스코드 Redirect URI를 실제 도메인으로 맞춘다.
|
|
- 이 디렉터리는 Next가 제공하는 에이전트 규칙 파일(`AGENTS.md`, `CLAUDE.md`)을 둔다 — 코드 수정 전 참고.
|