feat(page): 대시보드 디자인을 유튜브 뮤직(웹) 다크 테마로 전면 교체

- 상단 바: ☰ + 로고 + 반투명 검색창 + 프로필 메뉴(좁은 화면은 돋보기 → 전체 폭 검색)
- 왼쪽 메뉴: 홈/서버 목록 + 봇이 있는 내 서버 목록, 넓은 화면 펼침(240)/접힘(72), 좁은 화면 서랍
- 홈: 서버를 원형 카드로, 서버 페이지: 앨범 페이지 형태(왼쪽 아이콘·정보, 오른쪽 재생 목록)
- 검색: 필터 칩(전체/노래/동영상/Spotify) + 상위 검색결과 카드 + 구역별 목록(모두 표시)
- 하단 재생바: 위쪽 빨간 진행선, 조작·시간 / 곡 정보 / hover 볼륨·재생 화면(⌃)
- 재생 화면: 큰 앨범아트 + 다음 트랙 탭, 좁은 화면은 전체 화면 플레이어
- 재생/대기열/서버 로직을 hooks(usePlayer/useQueue/useServers)로 분리해 구독을 한 번만 연다
- 스낵바 알림, 파비콘, README 화면·버튼 문서 갱신

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 05:04:49 +09:00
parent 231dc6d716
commit a491a1a98e
16 changed files with 2026 additions and 1410 deletions

100
page/src/components/ui.tsx Normal file
View File

@@ -0,0 +1,100 @@
"use client";
import { Music, Server } from "lucide-react";
import type { DiscordServer } from "@/types/music";
import { serverIconUrl } from "@/hooks/useServers";
/** 유튜브 뮤직 로고 형태(빨간 원 + 흰 링 + 재생 삼각형) */
export function Logo({ className = "" }: { className?: string }) {
return (
<span className={`flex items-center gap-1 ${className}`}>
<svg viewBox="0 0 24 24" className="h-6 w-6 flex-shrink-0" aria-hidden>
<circle cx="12" cy="12" r="12" fill="#ff0000" />
<circle cx="12" cy="12" r="6.6" fill="none" stroke="#fff" strokeWidth="1.1" />
<path d="M10 9.2v5.6l4.9-2.8z" fill="#fff" />
</svg>
<span className="text-[20px] font-bold tracking-[-0.04em] text-white">MusicBot</span>
</span>
);
}
/** 유튜브 기본 썸네일(hq/mq/sd/0.jpg)은 4:3 안에 16:9 영상이 위아래 검은 띠와 함께 들어 있다. */
const LETTERBOXED_YT = /i\.ytimg\.com\/vi\/[^/]+\/(hqdefault|mqdefault|sddefault|0)\.jpg/;
/** 앨범아트/썸네일(이미지가 없으면 음표 아이콘) */
export function Thumb({
src,
className = "",
rounded = "rounded-sm",
iconSize = 18,
wide = false,
}: {
src?: string | null;
className?: string;
rounded?: string;
iconSize?: number;
/** 16:9 상자(동영상 행)면 true. 정사각형 상자에서만 검은 띠를 잘라낸다. */
wide?: boolean;
}) {
// 정사각형 상자에 4:3 유튜브 썸네일을 넣으면 검은 띠가 보이므로 4/3 배 확대해 띠를 잘라낸다.
const cropBars = !wide && !!src && LETTERBOXED_YT.test(src);
return (
<span className={`relative flex flex-shrink-0 items-center justify-center overflow-hidden bg-yt-raised ${rounded} ${className}`}>
{src ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={src} alt="" className={`h-full w-full object-cover ${cropBars ? "scale-[1.34]" : ""}`} />
) : (
<Music size={iconSize} className="text-white/40" aria-hidden />
)}
</span>
);
}
/** 디스코드 서버 아이콘(원형, 없으면 서버 아이콘) */
export function ServerAvatar({
server,
className = "",
iconSize = 20,
size = 128,
rounded = "rounded-full",
}: {
server: DiscordServer;
className?: string;
iconSize?: number;
size?: number;
rounded?: string;
}) {
const url = serverIconUrl(server, size);
return (
<span className={`flex flex-shrink-0 items-center justify-center overflow-hidden bg-yt-raised ${rounded} ${className}`}>
{url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={url} alt="" className="h-full w-full object-cover" />
) : (
<Server size={iconSize} className="text-white/50" aria-hidden />
)}
</span>
);
}
/** 유튜브 뮤직 스타일 원형 로딩 표시 */
export function Spinner({ size = 32, label = "불러오는 중" }: { size?: number; label?: string }) {
return (
<span
role="status"
aria-label={label}
className="inline-block animate-spin rounded-full border-[3px] border-white/15 border-t-white"
style={{ width: size, height: size }}
/>
);
}
/** 재생 중 이퀄라이저 표시 */
export function Equalizer({ paused = false, className = "" }: { paused?: boolean; className?: string }) {
return (
<span className={`yt-eq flex items-end gap-[2px] ${paused ? "paused" : ""} ${className}`} aria-hidden>
<span />
<span />
<span />
</span>
);
}