bot 전체 코드 품질 개선 및 버그 수정

- GuildPlayer: 타이머 레이스 컨디션 수정, 모든 타이머 정리 로직 통합 (clearAllTimers)
- GuildPlayer: 이벤트 핸들러에 try-catch 추가 (end, exception, stuck)
- GuildPlayer: start 이벤트에서 endTimer 정리, autoPlay tracks 길이 검증 추가
- RedisClient: player_seek, player_volume에 누락된 return ���가
- RedisClient: queue_remove 인덱스 검증 주석 명확화
- Handler: runCommand에 try-catch 추가하여 에러 시 사용자에게 응답
- Channel: getGuildById에 누락된 await 추가, getMemberById/getVoiceChannelById 안전한 에러 처리
- Command.d.ts: 잘못된 타입 ChatInputChatInputCommandInteraction → ChatInputCommandInteraction 수정
- join.ts: 채널 멘션 닫는 괄호 누락 수정
- shuffle.ts: 제네릭 타입 적용, 불필요한 5회 반복 제거
- import 경로 대소문자 수정 (Shuffle → shuffle) - Linux 호환
- YoutubeMusic/Spotify: 하드코딩된 IP를 환경변수로 분리
- console.log/error → Logger 통일 (YoutubeMusic, Button, channel)
- interactionCreate: 전체 try-catch 추가, silent catch에 로깅 추가
- Database: schema 경로 __dirname 기반으로 수정, 컬럼 화이트리스트 추가
- 사용하지 않는 코드 정리 (axios 의존성, 주석처리된 user 관련 코드)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 23:13:16 +09:00
parent fefdf1fcdb
commit d0dcdb1563
19 changed files with 178 additions and 162 deletions

View File

@@ -69,6 +69,8 @@ export const Config = {
return this._youtube_cookie;
},
proxyUrl: process.env.PROXY_URL?.trim() || "",
_redis: {
state: process.env.REDIS?.trim()?.toLocaleLowerCase() === "true",
host: process.env.REDIS_HOST?.trim(),

View File

@@ -7,12 +7,18 @@ import { Logger } from "./Logger";
const database = new Database(Config.dbPath);
const schemaPath = join(process.cwd(), "db/schema.sql");
const schemaPath = join(__dirname, "../../db/schema.sql");
const schema = readFileSync(schemaPath, "utf-8");
database.exec(schema);
Logger.ready("DB 활성화!");
// 허용되는 guild 테이블 컬럼 화이트리스<EBA6AC><EC8AA4><EFBFBD>
const GUILD_COLUMNS = new Set(["id", "name", "channel_id", "msg_id", "options"]);
const filterKeys = (keys: string[], whitelist: Set<string>) =>
keys.filter(k => whitelist.has(k));
const stmt = {
guild: {
// 전체
@@ -21,7 +27,7 @@ const stmt = {
get: database.prepare("SELECT * FROM guilds WHERE ID = ?"),
// 추가
insert: (data: GuildRow) => {
const keys = Object.keys(data);
const keys = filterKeys(Object.keys(data), GUILD_COLUMNS);
if (keys.length === 0) throw new Error("insert: 키1개는 있어야함");
return database.prepare(`INSERT INTO guilds (${
keys.map(k => `"${k}"`).join(", ")
@@ -31,35 +37,13 @@ const stmt = {
},
// 수정
update: (data: GuildRow) => {
const keys = Object.keys(data).filter(k => k !== "id");
const keys = filterKeys(Object.keys(data), GUILD_COLUMNS).filter(k => k !== "id");
if (keys.length === 0) throw new Error("update: 키1개는 있어야함");
return database.prepare(`UPDATE guilds SET ${
keys.map(k => `${k} = @${k}`).join(", ")
} WHERE id = @id`).run(data);
},
},
// user: {
// // 가져오기
// get: database.prepare("SELECT * FROM users WHERE guild_id = ? AND id = ?"),
// // 추가
// insert: (data: UserType) => {
// const keys = Object.keys(data);
// if (keys.length === 0) throw new Error("insert: 키1개는 있어야함");
// return database.prepare(`INSERT INTO users (${
// keys.map(k => `"${k}"`).join(", ")
// }) VALUES (${
// keys.map(k => `@${k}`).join(", ")
// })`).run(data);
// },
// // 수정
// update: (data: UserType) => {
// const keys = Object.keys(data).filter(k => k !== "guild_id" && k !== "id");
// if (keys.length === 0) throw new Error("update: 키1개는 있어야함");
// return database.prepare(`UPDATE users SET ${
// keys.map(k => `${k} = @${k}`).join(", ")
// } WHERE guild_id = @guild_id AND id = @id`).run(data);
// },
// },
};
export const DB = {
@@ -91,27 +75,4 @@ export const DB = {
}
},
},
// user: {
// get(guildId: string, id: string) {
// return stmt.user.get.get(guildId, id) as UserType | undefined;
// },
// set(data: UserType) {
// try {
// stmt.user.insert(data);
// return true;
// } catch (err) {
// Logger.error(String(err));
// return false;
// }
// },
// update(data: UserType) {
// try {
// stmt.user.update(data);
// return true;
// } catch (err) {
// Logger.error(String(err));
// return false;
// }
// },
// },
};

View File

@@ -8,7 +8,7 @@ const SPOTIFY_SECRET = process.env.SPOTIFY_SECRET?.trim() ?? "";
const SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token";
const SPOTIFY_API_URL = "https://api.spotify.com/v1";
const TOKENR_URL = "http://192.168.10.5:8075/api/token";
const TOKENR_URL = process.env.SPOTIFY_TOKENER_URL?.trim() || "http://192.168.10.5:8075/api/token";
const searchCache = new Map<string, string>();

View File

@@ -3,10 +3,11 @@ import crypto from "node:crypto";
import { Cookies } from "../../types/Youtube_Cookie";
import { Config } from "../Config";
import { SongItem } from "../../types/Track";
import { Logger } from "../Logger";
const customPREF = "tz=Asia.Seoul&hl=ko&gl=KR&last_quality=1080";
export const ORIGIN = "https://music.youtube.com";
const proxy = new ProxyAgent('http://192.168.10.4:3128');
const proxy = Config.proxyUrl ? new ProxyAgent(Config.proxyUrl) : undefined;
const searchCache = new Map<string, string>();
// 🌟 클래스 외부에 둘 상수 및 유틸리티 함수들 (내부에서만 사용됨)
@@ -56,7 +57,7 @@ export const YoutubeMusic = {
const missing = keys.filter((k) => !(k in cookies) && !(blocks ?? []).includes(k));
if (missing.length > 0) {
console.log("현재 입력된 쿠키 키 목록:", Object.keys(cookies));
Logger.warn(`현재 입력된 쿠키 키 목록: ${Object.keys(cookies).join(", ")}`);
throw new Error(`❌ 필수 인증 쿠키가 누락되었습니다: ${missing.join(", ")}`);
}
@@ -82,7 +83,7 @@ export const YoutubeMusic = {
* 완벽한 쿠키 인증과 서명(SAPISIDHASH)을 사용하여 유튜브 뮤직 검색을 수행합니다.
*/
async getSearchFull(query: string): Promise<SongItem[]> {
console.log(`🔍 [Auth-Cookie Engine] "${query}" 데이터 추출 중 (썸네일, 재생시간 포함)...`);
Logger.log(`🔍 [Auth-Cookie Engine] "${query}" 데이터 추출 중 (썸네일, 재생시간 포함)...`);
const url = "https://music.youtube.com/youtubei/v1/search?prettyPrint=false";
@@ -109,7 +110,7 @@ export const YoutubeMusic = {
query: query,
params: "EgWKAQIIAWoOEAMQBBAQEAkQFRAKEBE="
}),
dispatcher: proxy
...(proxy ? { dispatcher: proxy } : {})
});
const data: any = await response.json();
@@ -200,9 +201,9 @@ export const YoutubeMusic = {
}
}
return results || []; // 배열이 비어있을 경우 안전하게 null 반환
return results;
} catch (error) {
console.error("❌ getSearchFull 실행 중 에러:", error);
Logger.error(`❌ getSearchFull 실행 중 에러: ${String(error)}`);
return [];
}
},

View File

@@ -3,6 +3,7 @@ import { lavalinkManager } from "../../index";
import { checkTextChannelAndMsg, getTextChannelAndMsg } from "./Channel";
import { default_content, default_embed, default_image, getButtons } from "./Config";
import { DB } from "../Database";
import { Logger } from "../Logger";
export const buttonInteraction = (interaction: ButtonInteraction, args: string[]) => {
if (!interaction.guild) return;
@@ -16,7 +17,9 @@ export const buttonInteraction = (interaction: ButtonInteraction, args: string[]
} else {
if (args[0] === "recommend") buttonRecommend(interaction.guild);
}
return interaction.deferUpdate().catch(() => {});
return interaction.deferUpdate().catch((err) => {
Logger.warn(`[Button] deferUpdate 실패: ${String(err)}`);
});
}
const buttonRecommend = async (guild: Guild) => {
@@ -33,7 +36,7 @@ const buttonRecommend = async (guild: Guild) => {
components: [ getButtons() ],
files: [ default_image ],
}).catch((err) => {
console.error(err);
Logger.error(`[Button] 메세지 수정 실패: ${String(err)}`);
return null;
});
}

View File

@@ -6,15 +6,19 @@ import { clearAllMsg } from "./Utils";
import { client } from "../../index";
export const getGuildById = async (guildId: string): Promise<Guild | null> => {
const guild = client.guilds.cache.get(guildId)?.fetch();
const guild = await client.guilds.cache.get(guildId)?.fetch().catch(() => undefined);
if (!guild) return null;
return guild;
}
export const getMemberById = async (guild: Guild, userId: string): Promise<GuildMember | null> => {
const member = await guild.members.cache.get(userId)?.fetch(true);
if (!member) return null;
return member;
try {
const cached = guild.members.cache.get(userId);
if (!cached) return null;
return await cached.fetch(true);
} catch {
return null;
}
}
export const getVoiceChannel = (member: GuildMember): VoiceChannel | null => {
@@ -25,9 +29,14 @@ export const getVoiceChannel = (member: GuildMember): VoiceChannel | null => {
export const getVoiceChannelById = async (guild: Guild, userId: string): Promise<VoiceChannel | null> => {
if (!guild) return null;
const member = await guild.members.cache.get(userId)?.fetch(true);
if (!member) return null;
return getVoiceChannel(member);
try {
const cached = guild.members.cache.get(userId);
if (!cached) return null;
const member = await cached.fetch(true);
return getVoiceChannel(member);
} catch {
return null;
}
}
export const getTextChannelAndMsg = async (guild: Guild): Promise<{ channel?: TextChannel; msg?: Message; reason?: string; }> => {

View File

@@ -1,15 +1,8 @@
export const fshuffle = (list: any[]): any[] => {
var i, j, x;
for (i=list.length; i; i-=1) {
j = Math.floor(Math.random()*i);
x = list[i-1];
list[i-1] = list[j];
list[j] = x;
/** Fisher-Yates 셔플 (in-place) */
export const shuffle = <T>(list: T[]): T[] => {
for (let i = list.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[list[i], list[j]] = [list[j], list[i]];
}
return list;
}
export const shuffle = (list: any[]): any[] => {
for (let z=0; z<5; z++) list = fshuffle(list);
return list;
}