feat: 로그인·서버 선택 페이지 분리 + 플레이어 기능 추가(섞기·자동재생·주소 불러오기 등)
page - /login(디스코드 로그인 전용), /servers(서버 선택 전용), /servers/[id](유튜브 뮤직 플레이어)로 분리 · 서버 컴포넌트에서 로그인 검사, callbackUrl 은 같은 사이트 경로만 허용 · NextAuth 로그인/오류 화면도 /login 으로 - 서버 선택: 최근 사용 서버 맨 앞 "최근" 표시, 새로고침, 8개 초과 시 이름 검색, 오류 표시 - 플레이어: 검색어를 ?q= 로 남겨 뒤로가기 지원, 탭 제목에 지금 곡, 내 서버가 아니면 안내 - 검색창: 최근 검색어 목록, 유튜브·스포티파이 주소를 붙여넣으면 곡/재생목록 바로 불러오기 - 다음 트랙: 섞기, 모두 지우기(두 번 눌러 확인), 자동재생 스위치, 곡 수·총 길이 - 재생바: 처음부터 다시(⏮), 섞기 버튼 / 서버 홈: 음성채널 표시, 사용 안내, 최근 검색 칩 - 곡 추가 중 스피너 + 중복 추가 방지, 검색 실패 시 다시 시도, play/playlist 응답 대기 15초 - 계정 전환형 프로필 메뉴와 사이드바 서버 목록 제거(프로필은 서버 변경·로그아웃만) bot - Redis 액션 queue_shuffle, player_recommend 추가 - player_now 응답에 recommend(자동재생), voiceChannelName 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ import { channelJoin } from "../commands/join";
|
||||
import { GuildPlayer } from "./GuildPlayer";
|
||||
import { Guild, VoiceChannel } from "discord.js";
|
||||
import { SongItem } from "../types/Track";
|
||||
import { DB } from "../utils/Database";
|
||||
import { buttonRecommend } from "../utils/music/Button";
|
||||
|
||||
type SubAction =
|
||||
"search" |
|
||||
@@ -21,7 +23,9 @@ type SubAction =
|
||||
"player_volume" |
|
||||
"queue_list" |
|
||||
"queue_set" |
|
||||
"queue_remove";
|
||||
"queue_remove" |
|
||||
"queue_shuffle" |
|
||||
"player_recommend";
|
||||
|
||||
export const RedisClient = () => {
|
||||
if (Config.redis.state) return new RedisClientClass();
|
||||
@@ -141,7 +145,10 @@ class RedisClientClass {
|
||||
isPaused: player?.isPaused,
|
||||
position: player?.position,
|
||||
volume: player?.volume,
|
||||
track: player?.nowTrack ?? null
|
||||
track: player?.nowTrack ?? null,
|
||||
// 웹 대시보드 표시용: 자동재생 상태, 봇이 들어가 있는 음성채널 이름
|
||||
recommend: player ? player.isRecommend : (DB.guild.get(data.serverId)?.options.recommend ?? false),
|
||||
voiceChannelName: player ? (nowGuild.channels.cache.get(player.voiceChannelId)?.name ?? null) : null,
|
||||
}));
|
||||
}
|
||||
if (data.action === "queue_list") {
|
||||
@@ -194,6 +201,39 @@ class RedisClientClass {
|
||||
await this.reply(resultKey, 60, JSON.stringify({ success: true, removedTrack }));
|
||||
context.player.setMsg();
|
||||
}
|
||||
if (data.action === "queue_shuffle") {
|
||||
const resultKey = `queue:shuffle:${data.requestId}`;
|
||||
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
|
||||
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
|
||||
const context = await this.getContext(data.serverId, resultKey, data.userId);
|
||||
if (!context.ok) return;
|
||||
if (context.player.queue.length < 3) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "섞을 곡이 2곡 이상 있어야 합니다." }));
|
||||
context.player.setShuffle();
|
||||
await this.reply(resultKey, 60, JSON.stringify({ success: true }));
|
||||
}
|
||||
if (data.action === "player_recommend") {
|
||||
// 자동재생 토글. 재생 중이 아니어도 바꿀 수 있어야 하므로 음성채널 접속(getContext)을 거치지 않는다.
|
||||
const resultKey = `player:recommend:${data.requestId}`;
|
||||
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
|
||||
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
|
||||
const recGuild = await getGuildById(data.serverId);
|
||||
if (!recGuild) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
|
||||
if (!(await this.isMember(recGuild, data.userId))) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
|
||||
const player = lavalinkManager.getPlayer(recGuild.id);
|
||||
if (player) {
|
||||
// 재생 중이면 봇과 같은 음성채널에 있는 사람만 바꿀 수 있다(다른 조작과 같은 규칙).
|
||||
const voiceChannel = await getVoiceChannelById(recGuild, data.userId);
|
||||
if (!voiceChannel || voiceChannel.id !== player.voiceChannelId) {
|
||||
return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 조작할 수 있습니다." }));
|
||||
}
|
||||
player.setRecommend();
|
||||
} else {
|
||||
await buttonRecommend(recGuild);
|
||||
}
|
||||
const recommend = DB.guild.get(recGuild.id)?.options.recommend ?? false;
|
||||
await this.reply(resultKey, 60, JSON.stringify({ success: true, recommend }));
|
||||
this.publishState("player_update", { guildId: recGuild.id });
|
||||
}
|
||||
if (data.action === "player_paused") {
|
||||
const resultKey = `player:paused:${data.requestId}`;
|
||||
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
|
||||
|
||||
@@ -22,7 +22,7 @@ export const buttonInteraction = (interaction: ButtonInteraction, args: string[]
|
||||
});
|
||||
}
|
||||
|
||||
const buttonRecommend = async (guild: Guild) => {
|
||||
export const buttonRecommend = async (guild: Guild) => {
|
||||
const gdb = DB.guild.get(guild.id);
|
||||
const change = gdb ? DB.guild.update({...gdb, options: { ...gdb.options, recommend: !gdb.options.recommend }}) : false;
|
||||
if (!change) return;
|
||||
|
||||
Reference in New Issue
Block a user