검색해서 재생기능 제작

검색하고 재생 누르면 재생됨
플레이리스트 재생 기능(주소 전달해서 재생하는 방식)
This commit is contained in:
tkrmagid-desktop
2026-04-09 01:53:27 +09:00
parent cefe37e2a8
commit 2e014e9b34
11 changed files with 238 additions and 21 deletions

View File

@@ -15,13 +15,15 @@ export const authOptions: NextAuthOptions = {
},
callbacks: {
// 디스코드에서 받은 토큰(accessToken)을 우리 세션에 저장해두는 로직
async jwt({ token, account }) {
if (account) {
async jwt({ token, account, profile }) {
if (account && (profile as any)?.id) {
token.id = (profile as any).id;
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token }: any) {
session.user.id = token.id;
session.accessToken = token.accessToken;
return session;
},

View File

@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { Redis } from "@/lib/Redis";
export async function POST(request: Request) {
try {
const body = await request.json();
const { serverId, userId, track } = body;
if (!serverId) return NextResponse.json({ error: "serverId 정보가 필요합니다." }, { status: 400 });
if (!userId) return NextResponse.json({ error: "userId 정보가 필요합니다." }, { status: 400 });
if (!track) return NextResponse.json({ error: "track 정보가 필요합니다." }, { status: 400 });
const requestId = `req:${Date.now()}-${Math.random().toString(36).substring(7)}`;
const resultKey = `player:play:${requestId}`; // 봇이 대답을 남길 Redis 방 이름
// 봇에게 'player_play' 명령 전송
await Redis.publish("site-bot", JSON.stringify({
action: "player_play",
requestId: requestId,
serverId: serverId,
userId: userId,
track: track,
}));
// 3. 봇의 대답 기다리기 (최대 약 3초 대기)
for (let i = 0; i < 15; i++) {
await new Promise(resolve => setTimeout(resolve, 200)); // 0.2초씩 대기
const botReply = await Redis.get(resultKey);
if (botReply) {
// 봇이 대답을 남겼다면! 읽었으니 Redis에서 삭제하고 프론트로 전달
await Redis.del(resultKey);
const replyData = JSON.parse(botReply);
// replyData.success 가 false면 에러 상태코드(400)로 보냄
return NextResponse.json(replyData, { status: replyData.success ? 200 : 400 });
}
}
// 3초가 지나도 봇이 묵묵부답일 때
return NextResponse.json({ success: false, message: "봇이 응답하지 않거나 오프라인 상태입니다." }, { status: 504 });
} catch (error) {
console.error("Play API Error:", error);
return NextResponse.json({ error: "서버 오류가 발생했습니다." }, { status: 500 });
}
}

View File

@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { Redis } from "@/lib/Redis";
export async function POST(request: Request) {
try {
const body = await request.json();
const { serverId, userId, playlistUrl } = body;
if (!serverId) return NextResponse.json({ error: "serverId 정보가 필요합니다." }, { status: 400 });
if (!userId) return NextResponse.json({ error: "userId 정보가 필요합니다." }, { status: 400 });
if (!playlistUrl) return NextResponse.json({ error: "playlistUrl 정보가 필요합니다." }, { status: 400 });
const requestId = `req:${Date.now()}-${Math.random().toString(36).substring(7)}`;
const resultKey = `player:playlist:${requestId}`; // 봇이 대답을 남길 Redis 방 이름
// 봇에게 'player_playlist' 명령 전송
await Redis.publish("site-bot", JSON.stringify({
action: "player_playlist",
requestId: requestId,
serverId: serverId,
userId: userId,
playlistUrl: playlistUrl,
}));
// 3. 봇의 대답 기다리기 (최대 약 3초 대기)
for (let i = 0; i < 15; i++) {
await new Promise(resolve => setTimeout(resolve, 200)); // 0.2초씩 대기
const botReply = await Redis.get(resultKey);
if (botReply) {
// 봇이 대답을 남겼다면! 읽었으니 Redis에서 삭제하고 프론트로 전달
await Redis.del(resultKey);
const replyData = JSON.parse(botReply);
// replyData.success 가 false면 에러 상태코드(400)로 보냄
return NextResponse.json(replyData, { status: replyData.success ? 200 : 400 });
}
}
// 3초가 지나도 봇이 묵묵부답일 때
return NextResponse.json({ success: false, message: "봇이 응답하지 않거나 오프라인 상태입니다." }, { status: 504 });
} catch (error) {
console.error("Queue Adds API Error:", error);
return NextResponse.json({ error: "서버 오류가 발생했습니다." }, { status: 500 });
}
}

View File

@@ -13,7 +13,7 @@ export async function GET(request: Request) {
// 2. 고유한 요청 ID 생성 (예: 1690001234567-abc)
const requestId = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
const resultKey = `search:result:${requestId}`;
const resultKey = `search:${requestId}`;
// 3. 봇에게 'site-bot' 채널로 검색 명령 발송 (Publish)
await Redis.publish("site-bot", JSON.stringify({

View File

@@ -1,7 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { Play, ChevronLeft, Server, Music, Loader2, SearchX } from "lucide-react";
// 🌟 [수정됨] 상단 아이콘에 쓸 ListPlus 추가
import { Play, ChevronLeft, Server, Music, Loader2, SearchX, ListPlus } from "lucide-react";
import { ViewMode } from "@/app/page";
interface MainContentProps {
@@ -29,6 +30,47 @@ export default function MainContent({
const [searchResults, setSearchResults] = useState<any[]>([]);
const [isSearching, setIsSearching] = useState(false);
// 🌟 [추가됨] 봇에게 재생 및 대기열 추가 명령을 내리는 함수
const handleMusicAction = async (actionType: 'player_play' | 'player_playlist', track?: any, playlistUrl?: string) => {
if (!selectedServer) {
alert("명령을 내릴 디스코드 서버가 선택되지 않았습니다.");
return;
}
// 🌟 [추가된 부분] next-auth 세션에서 유저 식별자 가져오기
// (NextAuth 설정에 따라 id가 없을 수도 있으므로 없을 경우 name을 대체로 사용합니다)
const userId = (session?.user as any)?.id;
let endpoint = "";
let bodyData: any = { serverId: selectedServer.id, userId: userId };
if (actionType === 'player_play') {
endpoint = "/api/player/play";
bodyData.track = track;
} else if (actionType === 'player_playlist') {
endpoint = "/api/player/playlist";
bodyData.playlistUrl = playlistUrl;
}
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bodyData),
});
const data = await res.json();
if (res.ok && data.success) {
alert(data.message || "요청이 성공했습니다.");
} else {
alert(data.message || data.error || "요청 처리 중 문제가 발생했습니다.");
}
} catch (error) {
console.error("Music Action Error:", error);
alert('서버와 통신할 수 없습니다.');
}
};
// 권한 계산 함수
const getPermissionLabel = (server: any) => {
if (!server) return "알 수 없음";
@@ -192,13 +234,37 @@ export default function MainContent({
) : (
<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 key={track.videoId} className="bg-neutral-800/40 p-4 rounded-xl hover:bg-neutral-800 transition-all group border border-transparent hover:border-neutral-700 shadow-md">
{/* 🌟 썸네일 이미지 영역 (여기 안에 버튼 2개가 떠다닙니다) */}
<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">
{/* 🌟 [주석으로 제거] 우측 상단: 재생목록 추가 버튼 (위에서 아래로 스르륵) */}
{/* <button
onClick={(e) => {
e.stopPropagation(); // 부모 div 클릭 이벤트 방지
handleMusicAction('queue_add', track);
}}
className="absolute right-2 top-2 cursor-pointer bg-black/60 hover:bg-black/80 rounded-full p-2 opacity-0 group-hover:opacity-100 transition-all -translate-y-2 group-hover:translate-y-0 shadow-xl text-white backdrop-blur-sm"
title="재생목록에 추가"
>
<ListPlus fill="black" size={25} />
</button> */}
{/* 🌟 [수정됨] 우측 하단: 바로 재생 버튼 (기존 코드 유지 및 onClick 연결) */}
<button
onClick={(e) => {
e.stopPropagation();
handleMusicAction('player_play', track);
}}
className="absolute right-2 bottom-2 cursor-pointer bg-green-500 hover:bg-green-400 rounded-full p-3 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0 shadow-xl"
title="바로 재생"
>
<Play fill="black" stroke="black" size={20} />
</div>
</button>
</div>
<h3 className="font-semibold text-white truncate">{track.title}</h3>
<p className="text-sm text-neutral-400 truncate">{track.artist}</p>
</div>