지금까지 내용 커밋

This commit is contained in:
2026-04-08 12:59:45 +09:00
commit b0dae31cb9
68 changed files with 12083 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
"use client";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { Play, ChevronLeft, Server, Music, Loader2, SearchX } from "lucide-react";
import { ViewMode } from "@/app/page";
interface MainContentProps {
viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void;
selectedServer: any;
setSelectedServer: (server: any) => void;
searchQuery: string;
setSearchQuery: (query: string) => void;
onSelectServer: (server: any) => void;
}
export default function MainContent({
viewMode,
setViewMode,
selectedServer,
setSelectedServer,
searchQuery,
setSearchQuery,
onSelectServer,
}: MainContentProps) {
const { data: session, status } = useSession();
const [servers, setServers] = useState<any[]>([]);
const [isFetching, setIsFetching] = useState(false);
const [searchResults, setSearchResults] = useState<any[]>([]);
const [isSearching, setIsSearching] = useState(false);
// 권한 계산 함수
const getPermissionLabel = (server: any) => {
if (!server) return "알 수 없음";
if (server.owner) return "👑 서버 주인";
try {
const perms = BigInt(server.permissions);
if ((perms & BigInt(0x8)) === BigInt(0x8)) return "🛠️ 관리자";
if ((perms & BigInt(0x20)) === BigInt(0x20)) return "⚙️ 매니저";
return "👤 일반 멤버";
} catch {
return "👤 일반 멤버";
}
};
// 1. 서버 목록 불러오기
useEffect(() => {
if (status === "authenticated") {
setIsFetching(true);
const cached = sessionStorage.getItem("filtered_servers");
if (cached) {
setServers(JSON.parse(cached));
setIsFetching(false);
}
fetch("/api/servers")
.then((res) => res.json())
.then((data) => {
if (Array.isArray(data)) {
setServers(data);
sessionStorage.setItem("filtered_servers", JSON.stringify(data));
}
})
.finally(() => setIsFetching(false));
}
}, [status]);
// 2. 검색 결과 불러오기
useEffect(() => {
if (viewMode === "SEARCH_RESULT" && searchQuery) {
setIsSearching(true);
setSearchResults([]);
fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`)
.then(async (res) => {
const data = await res.json();
if (res.ok && Array.isArray(data)) {
setSearchResults(data);
} else {
console.error("Search Error:", data);
}
})
.catch(err => console.error("Fetch Error:", err))
.finally(() => setIsSearching(false));
}
}, [viewMode, searchQuery]);
// 로그인 안 된 상태
if (status === "unauthenticated") {
return (
<main className="flex-1 flex flex-col items-center justify-center bg-neutral-900">
<Server size={48} className="text-neutral-500 mb-4" />
<h2 className="text-2xl font-bold"> </h2>
<p className="text-neutral-400"> .</p>
</main>
);
}
// 세션 로딩 중
if (status === "loading") return <main className="flex-1 bg-neutral-900" />;
return (
<main className="flex-1 flex flex-col bg-gradient-to-b from-neutral-800/50 to-neutral-900 overflow-y-auto p-8">
{/* 화면 1: 서버 목록 */}
{viewMode === "SERVER_LIST" && (
<>
<h2 className="text-2xl font-bold mb-6"> </h2>
{isFetching && servers.length === 0 ? (
<div className="flex flex-col items-center py-20">
<Loader2 className="animate-spin text-green-500 mb-4" size={40} />
<p> ...</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{servers.map((server) => (
<div
key={server.id}
onClick={() => onSelectServer(server)}
className="bg-neutral-800/40 p-4 rounded-xl hover:bg-neutral-800 transition-all cursor-pointer group border border-transparent hover:border-neutral-700 shadow-md"
>
<div className="aspect-square bg-neutral-700 rounded-full mb-4 overflow-hidden shadow-lg flex items-center justify-center">
{server.icon ? (
<img src={`https://cdn.discordapp.com/icons/${server.id}/${server.icon}.png`} className="w-full h-full object-cover" alt="" />
) : <Server size={32} className="text-neutral-500" />}
</div>
<h3 className="font-semibold text-center truncate">{server.name}</h3>
</div>
))}
</div>
)}
</>
)}
{/* 화면 2: 서버 상세 대시보드 */}
{viewMode === "SERVER_DETAIL" && selectedServer && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
<button onClick={() => {
setViewMode("SERVER_LIST");
setSelectedServer(null);
setSearchQuery("");
}} className="flex items-center gap-1 text-neutral-400 hover:text-white mb-6 cursor-pointer group">
<ChevronLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
</button>
<div className="flex items-end gap-6 mb-8">
<div className="w-48 h-48 bg-neutral-800 rounded-2xl shadow-2xl flex items-center justify-center overflow-hidden">
{selectedServer.icon ? (
<img src={`https://cdn.discordapp.com/icons/${selectedServer.id}/${selectedServer.icon}.png`} className="w-full h-full object-cover" alt="" />
) : <Server size={64} className="text-neutral-500" />}
</div>
<div>
<span className="text-sm font-bold uppercase text-neutral-400 tracking-wider"> </span>
<h1 className="text-5xl font-black mt-2 mb-4">{selectedServer.name}</h1>
<p className="text-neutral-400"> .</p>
</div>
</div>
<div className="bg-black/20 rounded-xl p-6 border border-neutral-800">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2"><Music size={20} /> </h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="bg-neutral-800/50 p-4 rounded-lg border border-white/5">
<p className="text-neutral-500 mb-1"> ID</p>
<p className="font-mono text-white">{selectedServer.id}</p>
</div>
<div className="bg-neutral-800/50 p-4 rounded-lg border border-white/5">
<p className="text-neutral-500 mb-1"> </p>
<p className="text-green-400 font-bold">{getPermissionLabel(selectedServer)}</p>
</div>
</div>
</div>
</div>
)}
{/* 화면 3: 검색 결과 목록 */}
{viewMode === "SEARCH_RESULT" && (
<div>
<h2 className="text-2xl font-bold mb-2">"{searchQuery}" </h2>
<p className="text-neutral-400 mb-8 text-sm"> .</p>
{isSearching ? (
<div className="flex flex-col items-center py-20">
<Loader2 className="animate-spin text-green-500 mb-4" size={40} />
<p> ...</p>
</div>
) : searchResults.length === 0 ? (
<div className="flex flex-col items-center py-20 text-neutral-500">
<SearchX size={48} className="mb-4" />
<p> .</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{searchResults.map((track) => (
<div key={track.videoId} className="bg-neutral-800/40 p-4 rounded-xl hover:bg-neutral-800 transition-all cursor-pointer group border border-transparent hover:border-neutral-700 shadow-md">
<div className="aspect-square bg-neutral-700 rounded-md mb-4 relative overflow-hidden shadow-lg">
{track.thumbnail && <img src={track.thumbnail} className="w-full h-full object-cover" alt={track.title} />}
<div className="absolute right-2 bottom-2 bg-green-500 rounded-full p-3 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0 shadow-xl">
<Play fill="black" stroke="black" size={20} />
</div>
</div>
<h3 className="font-semibold text-white truncate">{track.title}</h3>
<p className="text-sm text-neutral-400 truncate">{track.artist}</p>
</div>
))}
</div>
)}
</div>
)}
</main>
);
}

View File

@@ -0,0 +1,47 @@
"use client";
import { SkipForward, SkipBack, Volume2, Pause } from "lucide-react";
export default function PlayerBar() {
return (
<footer className="h-24 flex-shrink-0 bg-black border-t border-neutral-800 px-6 flex items-center justify-between z-50">
<div className="flex items-center gap-4 w-1/4">
<div className="w-14 h-14 bg-neutral-800 rounded-md shadow-md"></div>
<div className="flex flex-col">
<span className="text-sm font-bold text-white hover:underline cursor-pointer"> </span>
<span className="text-xs text-neutral-400 hover:underline cursor-pointer">(IU)</span>
</div>
</div>
<div className="flex flex-col items-center gap-2 w-2/4 max-w-2xl">
<div className="flex items-center gap-6">
<button className="text-neutral-400 hover:text-white transition-colors">
<SkipBack size={20} />
</button>
<button className="w-8 h-8 rounded-full bg-white flex items-center justify-center hover:scale-105 transition-transform text-black">
<Pause size={18} fill="black" />
</button>
<button className="text-neutral-400 hover:text-white transition-colors">
<SkipForward size={20} />
</button>
</div>
<div className="flex items-center gap-2 w-full text-xs text-neutral-400">
<span>01:12</span>
<div className="h-1 bg-neutral-600 rounded-full flex-1 cursor-pointer group">
<div className="h-full bg-white rounded-full w-1/3 group-hover:bg-green-500 transition-colors relative">
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full opacity-0 group-hover:opacity-100 shadow-md"></div>
</div>
</div>
<span>03:16</span>
</div>
</div>
<div className="flex items-center justify-end gap-3 w-1/4 text-neutral-400">
<Volume2 size={20} />
<div className="w-24 h-1 bg-neutral-600 rounded-full cursor-pointer group">
<div className="h-full bg-white rounded-full w-2/3 group-hover:bg-green-500 transition-colors"></div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,24 @@
"use client";
export default function QueueSidebar() {
return (
<aside className="w-80 bg-black p-6 border-l border-neutral-800 flex flex-col">
<h2 className="text-lg font-bold mb-6"> </h2>
<div className="flex flex-col gap-4 overflow-y-auto">
{[1, 2, 3, 4, 5].map((item) => (
<div key={item} className="flex items-center gap-3 group cursor-pointer">
<div className="w-10 h-10 bg-neutral-800 rounded flex-shrink-0"></div>
<div className="flex flex-col overflow-hidden">
<span className="text-sm font-medium text-white truncate group-hover:text-green-500 transition-colors">
{item === 1 ? "Hype Boy" : "Supernova"}
</span>
<span className="text-xs text-neutral-400 truncate">
{item === 1 ? "NewJeans" : "aespa"}
</span>
</div>
</div>
))}
</div>
</aside>
);
}