page 전체 코드 품질/보안 개선 및 봇 RPC 검증 정합
[보안/인증] - 모든 player/queue API 라우트에 세션 가드 추가 (이전: /api/servers 만 보호) - NextAuth 환경변수 부팅 시점 검증, NEXTAUTH_SECRET 명시 - next.config.ts CSP/보안 헤더 추가, 잘못된 allowedDevOrigins 제거 - Redis 호스트 하드코딩 IP 제거(필수 env 로 강제) [안정성] - 봇 RPC 패턴(@/lib/api) 공용화: crypto.randomUUID requestId, JSON.parse 안전, EXPIRE 자동, 폴링 백오프 - SSE(@/lib/sse) 공용화: subscriber error 처리, JSON.parse 안전, 30초 keep-alive, abort/에러 정리 - pause API 양 끝(boolean) 정상화: 프론트 String() 캐스트 + 백엔드 .trim().toLowerCase() 비교 제거 - 봇 RedisClient: isPaused/index/seek/volume falsy 거부 → typeof 검사로 교체(0/false 정상 허용) [타입/품질] - next-auth 모듈 보강 → session.user.id, session.accessToken 타입 안전 - DiscordServer/Track/SearchTrack 공용 타입 도입, 컴포넌트 any 제거 - BigInt permissions 안전 검증(타입 가드) - Logger: NODE_ENV 게이트, error → stderr, ISO 기반 안전 timestamp - tsconfig target → ES2020 (BigInt 리터럴) [취약점] - next 16.2.2 → 16.2.4 (DoS/postcss XSS 패치) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,21 +5,16 @@ import { Play, ChevronLeft, Server, Music, Loader2, SearchX, MonitorPlay, Disc }
|
||||
import { ViewMode } from "@/app/page";
|
||||
// 🌟 [추가됨] 전역 토스트 훅 불러오기
|
||||
import { useToast } from "@/components/ToastProvider";
|
||||
import type { DiscordServer, SearchTrack, SearchResults } from "@/types/music";
|
||||
|
||||
interface MainContentProps {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
selectedServer: any;
|
||||
setSelectedServer: (server: any) => void;
|
||||
selectedServer: DiscordServer | null;
|
||||
setSelectedServer: (server: DiscordServer | null) => void;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
onSelectServer: (server: any) => void;
|
||||
}
|
||||
|
||||
interface SearchResultsType {
|
||||
spotify: any[];
|
||||
youtubeMusic: any[];
|
||||
youtubeVideo: any[];
|
||||
onSelectServer: (server: DiscordServer) => void;
|
||||
}
|
||||
|
||||
export default function MainContent({
|
||||
@@ -36,27 +31,31 @@ export default function MainContent({
|
||||
// 🌟 [추가됨] 훅을 실행해서 showToast 함수 꺼내기
|
||||
const { showToast } = useToast();
|
||||
|
||||
const [servers, setServers] = useState<any[]>([]);
|
||||
const [servers, setServers] = useState<DiscordServer[]>([]);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<SearchResultsType>({
|
||||
const [searchResults, setSearchResults] = useState<SearchResults>({
|
||||
spotify: [],
|
||||
youtubeMusic: [],
|
||||
youtubeVideo: []
|
||||
});
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const handleMusicAction = async (actionType: 'player_play' | 'player_playlist', track?: any, playlistUrl?: string) => {
|
||||
const handleMusicAction = async (actionType: 'player_play' | 'player_playlist', track?: SearchTrack, playlistUrl?: string) => {
|
||||
if (!selectedServer) {
|
||||
// 🌟 alert -> showToast 교체
|
||||
showToast("명령을 내릴 디스코드 서버가 선택되지 않았습니다.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = (session?.user as any)?.id;
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) {
|
||||
showToast("로그인이 필요합니다.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
let endpoint = "";
|
||||
let bodyData: any = { serverId: selectedServer.id, userId: userId };
|
||||
const bodyData: Record<string, unknown> = { serverId: selectedServer.id, userId };
|
||||
if (actionType === 'player_play') {
|
||||
endpoint = "/api/player/play";
|
||||
bodyData.track = track;
|
||||
@@ -88,13 +87,16 @@ export default function MainContent({
|
||||
}
|
||||
};
|
||||
|
||||
const getPermissionLabel = (server: any) => {
|
||||
const getPermissionLabel = (server: DiscordServer | null) => {
|
||||
if (!server) return "알 수 없음";
|
||||
if (server.owner) return "👑 서버 주인";
|
||||
// Discord permissions 는 큰 정수 문자열로 도착함. 숫자/문자열만 받아 안전하게 BigInt 화.
|
||||
const raw: unknown = server.permissions;
|
||||
if (typeof raw !== "string" && typeof raw !== "number") return "👤 일반 멤버";
|
||||
try {
|
||||
const perms = BigInt(server.permissions);
|
||||
if ((perms & BigInt(0x8)) === BigInt(0x8)) return "🛠️ 관리자";
|
||||
if ((perms & BigInt(0x20)) === BigInt(0x20)) return "⚙️ 매니저";
|
||||
const perms = BigInt(raw);
|
||||
if ((perms & 0x8n) === 0x8n) return "🛠️ 관리자";
|
||||
if ((perms & 0x20n) === 0x20n) return "⚙️ 매니저";
|
||||
return "👤 일반 멤버";
|
||||
} catch {
|
||||
return "👤 일반 멤버";
|
||||
@@ -159,7 +161,7 @@ export default function MainContent({
|
||||
|
||||
const hasAnyResults = searchResults.spotify.length > 0 || searchResults.youtubeMusic.length > 0 || searchResults.youtubeVideo.length > 0;
|
||||
|
||||
const renderTrackCard = (track: any) => (
|
||||
const renderTrackCard = (track: SearchTrack) => (
|
||||
<div key={track.videoId || track.id} className="bg-neutral-800/40 p-3 rounded-xl hover:bg-neutral-800 transition-all group border border-transparent hover:border-neutral-700 shadow-md w-full">
|
||||
<div className="aspect-square bg-neutral-700 rounded-md mb-2 relative overflow-hidden shadow-lg">
|
||||
{track.thumbnail && <img src={track.thumbnail} className="w-full h-full object-cover" alt={track.title} />}
|
||||
@@ -258,7 +260,7 @@ export default function MainContent({
|
||||
{/* 화면 3: 검색 결과 목록 (3가지 카테고리로 분할) */}
|
||||
{viewMode === "SEARCH_RESULT" && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">"{searchQuery}" 검색 결과</h2>
|
||||
<h2 className="text-2xl font-bold mb-2">“{searchQuery}” 검색 결과</h2>
|
||||
<p className="text-neutral-400 mb-8 text-sm">입력하신 검색어에 대한 플랫폼별 결과입니다.</p>
|
||||
|
||||
{isSearching ? (
|
||||
|
||||
Reference in New Issue
Block a user