diff --git a/page/README.md b/page/README.md
index f7eca69..f151381 100644
--- a/page/README.md
+++ b/page/README.md
@@ -78,65 +78,65 @@ site: "bot-site" 구독 → serverId 일치하는 것만 EventSource(SSE)로 브
## 5. UI 구성 (`src/`)
-- `app/layout.tsx`, `app/page.tsx` — 루트 레이아웃/페이지. `components/Providers.tsx`(세션 등), `ToastProvider.tsx`.
-- `components/layout/` — `TopNav`(상단/서버선택·로그인), `LeftSidebar`(탐색).
-- `components/player/` — `PlayerBar`(재생바/볼륨/탐색), `MainContent`(검색·결과), `QueueSidebar`(대기열).
+디자인은 **유튜브 뮤직(웹) 다크 테마**를 그대로 따른다: 배경 `#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. 화면(모드)별 사용 흐름
+### 5-1. 화면 구성
화면은 라우트가 아니라 `app/page.tsx`의 `ViewMode` 상태로 전환된다(SPA 단일 페이지).
| 화면 | 진입 방법 | 화면에 보이는 것 | 여기서 할 수 있는 일 |
|------|-----------|------------------|----------------------|
-| 로그인 전 | 첫 방문 | 중앙 로그인 카드 | 디스코드 로그인. 사이드바/대기열/재생바는 렌더링하지 않는다 |
-| `SERVER_LIST` | 로그인 직후, 홈 버튼, "다른 서버 선택" | 봇이 있는 내 서버 카드 목록 | 서버 선택. 서버를 고르기 전에는 검색·재생이 잠겨 있다 |
-| `SERVER_DETAIL` | 서버 카드 클릭 | 서버 이름/아이콘, 사용 안내 배너, 서버 ID·내 권한 | 검색 시작점. 좌측에 조작 중인 서버 표시 |
-| `SEARCH_RESULT` | 검색창에 입력 후 Enter | Spotify / YouTube Music / YouTube 영상 3개 구역의 곡 리스트 행(번호·썸네일·제목/아티스트·재생시간) | 행을 눌러 대기열에 추가 |
-| 대기열 패널 | (넓은 화면) 기본 펼침, 재생바 우측 대기열 버튼으로 접기/펴기 · (좁은 화면) 기본 접힘, 우하단 초록 버튼·재생바 목록 버튼으로 열기 | "지금 재생 중" + "다음 재생 목록" | 순서 변경, 삭제, 특정 곡부터 재생 |
+| 로그인 전 | 첫 방문 | 가운데 로그인 안내, 우상단 "로그인" | 디스코드 로그인. 검색·재생바는 잠김/숨김 |
+| `SERVER_LIST` (홈) | 로그인 직후, 왼쪽 메뉴 "서버 목록" | 봇이 있는 내 서버를 원형 카드로(유튜브 뮤직 아티스트 줄 형태) | 서버 선택 |
+| `SERVER_DETAIL` (서버 페이지) | 서버 카드/왼쪽 메뉴의 서버, 로고·"홈" | 유튜브 뮤직 앨범 페이지 형태: 왼쪽 서버 아이콘·이름·권한·사용 안내, 오른쪽 "재생 목록"(지금 재생 중 + 다음 트랙) | 검색 시작, 대기열 조작 |
+| `SEARCH_RESULT` | 검색창 Enter | 필터 칩(전체/노래/동영상/Spotify), 상위 검색결과 카드, 구역별 곡 목록(전체 탭은 5곡씩 + "모두 표시") | 곡을 눌러 대기열에 추가 |
+| 재생 화면 | 재생바의 곡 정보·⌃ 버튼, 서버 페이지의 목록 아이콘 | 넓은 화면: 큰 앨범아트 + "다음 트랙" 탭 / 좁은 화면: 전체 화면 플레이어(진행바·조작·볼륨) + 다음 트랙 | 대기열 조작, (좁은 화면) 재생 조작 |
+
+- 재생바는 유튜브 뮤직처럼 **재생 중인 곡이 있을 때만** 나타난다.
+- 왼쪽 메뉴: 넓은 화면은 ☰ 로 펼침(240px)/접힘(아이콘만 72px), 좁은 화면은 ☰ 로 서랍을 연다.
+- 좁은 화면 상단 바는 검색창 대신 돋보기 버튼 → 누르면 바 전체가 검색창이 된다.
### 5-2. 버튼·조작 동작 정리
| 위치 | 요소 | 동작 | 비활성 조건 |
|------|------|------|-------------|
-| 상단 | 홈 | 서버를 고른 상태면 서버 상세로, 아니면 서버 목록으로. 검색어 초기화 | 로그아웃 상태에선 숨김 |
-| 상단 | 검색창 | Enter 로 검색 실행 → `SEARCH_RESULT` | 로그아웃·서버 미선택 시 잠금(안내 문구가 상황별로 바뀜) |
-| 상단 | 프로필/로그아웃 | 세션 종료 | — |
-| 본문 | 서버 카드 | 서버 선택 → 서버 상세 | — |
-| 본문 | 곡 행(행 전체가 버튼) | `POST /api/player/play` → 대기열 추가. 성공/실패를 토스트로 알림. 넓은 화면은 hover 시 번호 자리에 재생 아이콘이, 좁은 화면은 썸네일에 재생 아이콘이 항상 보인다 | 서버 미선택·미로그인 |
-| 재생바 | 재생/일시정지 | `POST /api/player/pause` (낙관적 UI, 실패 시 롤백) | 봇 세션 없음/재생 중 아님 |
-| 재생바 | 다음 곡 | `POST /api/player/skip` | 〃 |
+| 상단 | ☰ | 왼쪽 메뉴 펼침/접힘(좁은 화면은 서랍) | — |
+| 상단 | 로고 | 서버를 고른 상태면 서버 페이지로, 아니면 홈으로 | — |
+| 상단 | 검색창 | 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 에 없음). 이유를 툴팁으로 표시하고 비활성 | 항상 |
-| 재생바 | 진행바 | 드래그/클릭으로 탐색 → `POST /api/player/seek`. 좁은 화면에서는 재생바 상단에 전체 폭으로 배치 | 〃 |
-| 재생바 | 스피커 아이콘 | 음소거/해제 토글(해제 시 직전 볼륨 복원) | 〃 |
-| 재생바 | 볼륨 슬라이더 | 드래그 중에는 화면만, 손을 뗄 때 `POST /api/player/volume` 1회 전송 | 〃 |
-| 재생바 | 목록 아이콘(컨트롤 옆, 좁은 화면) | 대기열 슬라이드 패널 열기/닫기 | — |
-| 재생바 | 목록 아이콘(우측 볼륨 옆, 넓은 화면) | 우측 대기열 컬럼 접기/펴기. 펼쳐져 있으면 초록색으로 표시 | — |
-| 대기열 | 곡(제목 영역) | 그 곡부터 재생. 앞선 곡들은 대기열에서 빠진다 | 첫 곡은 이미 다음 곡이라 동작 없음 |
-| 대기열 | ▲ ▼ | 한 칸씩 순서 이동 → `POST /api/queue/set` (좁은 화면 전용, 드래그 대체) | 맨 위/맨 아래 |
-| 대기열 | 휴지통 | 해당 곡 제거 → `POST /api/queue/remove` (`encoded` 동봉해 엉뚱한 곡 삭제 방지) | — |
-| 대기열 | 드래그 핸들 | 드래그로 순서 변경(넓은 화면 전용) | — |
+| 재생바 | 곡 정보 / ⌃ | 재생 화면 열기·닫기 | — |
+| 재생바 | 스피커 아이콘 | 클릭 = 음소거/해제(해제 시 직전 볼륨 복원). hover 시 왼쪽에 볼륨 슬라이더가 펼쳐짐 | 봇 세션 없음/재생 중 아님 |
+| 재생바·재생 화면 | 볼륨 슬라이더 | 드래그 중에는 화면만, 손을 뗄 때 `POST /api/player/volume` 1회 전송 | 〃 |
+| 다음 트랙 | 곡 | 그 곡부터 재생. 앞선 곡들은 대기열에서 빠진다(첫 곡이면 다음 곡으로 넘기기만) | — |
+| 다음 트랙 | 손잡이 드래그 | 순서 변경(마우스 전용, hover 시 표시) | — |
+| 다음 트랙 | ▲ ▼ | 한 칸씩 순서 이동 → `POST /api/queue/set` (터치 화면 전용, 드래그 대체) | 맨 위/맨 아래 |
+| 다음 트랙 | 휴지통 | 해당 곡 제거 → `POST /api/queue/remove` (`encoded` 동봉해 엉뚱한 곡 삭제 방지). 마우스면 hover 때, 터치 화면이면 항상 표시 | — |
+| 재생 화면 | ⌄ / Esc | 재생 화면 닫기 | — |
| 전역 | 키보드 | `Space` 재생/일시정지, `M` 음소거, `←`/`→` 5초 탐색, `Ctrl/⌘+→` 다음 곡 | 입력창 포커스 중에는 가로채지 않음 |
-### 5-3. 다른 음악 서비스와 비교해 맞춘 점
-
-스포티파이 웹 플레이어 / 유튜브 뮤직 / 애플 뮤직에서 공통으로 쓰이는 관행을 기준으로 삼았다.
-
-| 관행 | 예전 | 현재 |
-|------|------|------|
-| 곡 검색 결과 표시 | 그리드 카드 | 리스트 행(번호·썸네일·제목/아티스트·재생시간). 그리드는 앨범/재생목록처럼 커버가 주인공일 때 쓰는 형태이고, 개별 곡은 두 서비스 모두 행 형식이다 |
-| 대기열 접기/펴기 | 넓은 화면에서 항상 고정 | 재생바 우측 대기열 버튼으로 접고 펼 수 있음(스포티파이와 같은 위치) |
-| 좁은 화면 대응 | 3열 고정이라 모바일에서 본문이 눌려 사용 불가 | 단일 컬럼 + 대기열 슬라이드 패널 |
-| 대기열 상단에 "지금 재생 중" | 없음(다음 곡만) | 현재 곡을 강조해 표시 |
-| 대기열 곡 클릭 → 그 곡부터 재생 | 없음 | 지원(앞 곡들은 빠짐 — 스포티파이 대기열과 동일) |
-| 스피커 아이콘 클릭 = 음소거 | 장식 아이콘 | 토글 버튼 |
-| 키보드 단축키(Space/M/화살표) | 없음 | 지원 |
-| 재생시간 표기 | 없음 | 검색 결과 행 오른쪽에 표시 |
-| 터치 조작 크기(44px 내외) | 재생 32px·삭제 24px·진행바 1px | 44~48px, 진행바 히트영역 확대 |
-| hover 없이도 조작 가능 | 삭제·재생 버튼이 hover 시에만 노출 | 상시 노출 |
-
-미지원(봇 API 한계): 이전 곡, 반복 재생, 특정 위치로의 셔플 재생. 필요하면 봇 쪽 액션 추가가 선행돼야 한다.
+미지원(봇 API 한계): 이전 곡, 반복 재생, 셔플, 좋아요/가사. 유튜브 뮤직에 있는 이 버튼들은 동작하지 않는 가짜 버튼이 되므로 넣지 않았다.
---
diff --git a/page/src/app/globals.css b/page/src/app/globals.css
index 8f4a3a3..0cb5684 100644
--- a/page/src/app/globals.css
+++ b/page/src/app/globals.css
@@ -1,10 +1,25 @@
-/* Tailwind v4의 핵심! 이 한 줄로 모든 유틸리티 클래스를 불러옵니다. */
@import "tailwindcss";
-/* 다크 모드 기반의 플레이어이므로 배경을 기본적으로 어둡게 세팅합니다. */
+/*
+ * 유튜브 뮤직(웹) 다크 테마 색상.
+ * - 배경 #030303, 재생바/메뉴 #212121, 강조 빨강 #ff0000
+ * - 보조 글자는 흰색 70%/50% 투명도(유튜브 뮤직과 같은 방식)
+ */
@theme {
- --color-background: #171717; /* neutral-900 */
+ --color-background: #030303;
--color-foreground: #ffffff;
+ --color-yt-bg: #030303;
+ --color-yt-raised: #212121;
+ --color-yt-menu: #282828;
+ --color-yt-red: #ff0000;
+ --color-yt-chip: rgb(255 255 255 / 0.1);
+ --color-yt-chip-hover: rgb(255 255 255 / 0.2);
+ --color-yt-line: rgb(255 255 255 / 0.1);
+ --font-sans: Roboto, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", Arial, sans-serif;
+}
+
+html {
+ color-scheme: dark;
}
body {
@@ -12,19 +27,54 @@ body {
color: var(--color-foreground);
margin: 0;
padding: 0;
+ -webkit-font-smoothing: antialiased;
}
-/* 스크롤바 예쁘게 숨기기 (선택 사항) */
+/* 유튜브 뮤직처럼 얇고 어두운 스크롤바 */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: rgb(255 255 255 / 0.2) transparent;
+}
::-webkit-scrollbar {
width: 8px;
+ height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
- background: #3f3f46;
+ background: rgb(255 255 255 / 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
- background: #52525b;
-}
\ No newline at end of file
+ background: rgb(255 255 255 / 0.3);
+}
+
+/* 가로 스크롤 칩 줄은 스크롤바를 숨긴다 */
+.no-scrollbar {
+ scrollbar-width: none;
+}
+.no-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
+/* 재생 중 표시(이퀄라이저 막대) */
+@keyframes yt-eq {
+ 0%, 100% { transform: scaleY(0.3); }
+ 50% { transform: scaleY(1); }
+}
+.yt-eq > span {
+ display: block;
+ width: 3px;
+ height: 14px;
+ background: currentColor;
+ transform-origin: bottom;
+ animation: yt-eq 0.9s ease-in-out infinite;
+}
+.yt-eq > span:nth-child(2) { animation-delay: -0.3s; }
+.yt-eq > span:nth-child(3) { animation-delay: -0.6s; }
+.yt-eq.paused > span { animation-play-state: paused; }
+
+@media (prefers-reduced-motion: reduce) {
+ .yt-eq > span { animation: none; transform: scaleY(0.6); }
+}
diff --git a/page/src/app/icon.svg b/page/src/app/icon.svg
new file mode 100644
index 0000000..0cccbde
--- /dev/null
+++ b/page/src/app/icon.svg
@@ -0,0 +1 @@
+
diff --git a/page/src/app/page.tsx b/page/src/app/page.tsx
index 637adb3..e99083f 100644
--- a/page/src/app/page.tsx
+++ b/page/src/app/page.tsx
@@ -2,151 +2,132 @@
import { useCallback, useState } from "react";
import { useSession } from "next-auth/react";
-import { ListMusic, X } from "lucide-react";
import TopNav from "@/components/layout/TopNav";
import LeftSidebar from "@/components/layout/LeftSidebar";
import MainContent from "@/components/player/MainContent";
-import QueueSidebar from "@/components/player/QueueSidebar";
import PlayerBar from "@/components/player/PlayerBar";
-import type { DiscordServer, Track } from "@/types/music";
+import PlayerPage from "@/components/player/PlayerPage";
+import { usePlayer } from "@/hooks/usePlayer";
+import { useQueue } from "@/hooks/useQueue";
+import { useServers } from "@/hooks/useServers";
+import type { DiscordServer } from "@/types/music";
// 화면 모드 타입 정의
export type ViewMode = "SERVER_LIST" | "SERVER_DETAIL" | "SEARCH_RESULT";
+/**
+ * 유튜브 뮤직(웹) 구조:
+ * 상단 바(☰ 로고 · 검색 · 프로필)
+ * [왼쪽 메뉴] [본문] ← 재생 화면(⌃)은 본문 위를 덮는다
+ * 하단 재생바
+ */
export default function MusicPlayerLayout() {
const { status } = useSession();
const [viewMode, setViewMode] = useState("SERVER_LIST");
const [selectedServer, setSelectedServer] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
- // 대기열 표시 상태는 화면 크기별로 기본값이 달라서 두 개로 나눠 둔다.
- // (창 크기를 읽어 한 상태로 합치면 SSR·하이드레이션이 어긋난다)
- // · 좁은 화면: 기본 접힘 → 버튼으로 슬라이드 패널을 연다
- // · 넓은 화면: 기본 펼침 → 재생바의 대기열 버튼으로 접을 수 있다
- const [isQueueOpen, setIsQueueOpen] = useState(false);
- const [isQueueDocked, setIsQueueDocked] = useState(true);
- // 재생바가 이미 폴링하는 현재 곡을 대기열 패널과 공유한다(같은 API 를 두 번 부르지 않기 위함).
- const [nowPlaying, setNowPlaying] = useState