fix: 음악자동재생 + 버그수정

develop
김성경 2026-06-18 13:08:51 +09:00
parent 2988a59749
commit 0e5a63a28b
3 changed files with 45 additions and 23 deletions

View File

@ -53,7 +53,7 @@ function IconMusic() {
</svg> </svg>
); );
} }
function IconVolume({ level }: { level: number; muted: boolean }) { function IconVolume({ level }: { level: number }) {
if (level === 0) { if (level === 0) {
return ( return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@ -145,10 +145,10 @@ export default function MusicBar() {
loadPlaylist, loadPlaylist,
} = usePlayer(); } = usePlayer();
// 경로가 바뀌면 해당 경로의 플레이리스트 로드 // 경로가 바뀌면 해당 경로의 플레이리스트 로드, 없으면 초기화
useEffect(() => { useEffect(() => {
const tracks = PATH_PLAYLISTS[pathname]; const tracks = PATH_PLAYLISTS[pathname];
if (tracks) loadPlaylist(tracks); loadPlaylist(tracks ?? []);
}, [pathname, loadPlaylist]); }, [pathname, loadPlaylist]);
if (!hasTrack) return null; if (!hasTrack) return null;
@ -171,9 +171,10 @@ export default function MusicBar() {
}; };
const handleVolumeDrag = (e: React.PointerEvent<HTMLDivElement>) => { const handleVolumeDrag = (e: React.PointerEvent<HTMLDivElement>) => {
e.currentTarget.setPointerCapture(e.pointerId); const el = e.currentTarget;
el.setPointerCapture(e.pointerId);
const update = (ev: PointerEvent) => { const update = (ev: PointerEvent) => {
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect(); const rect = el.getBoundingClientRect();
setVolume(Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width))); setVolume(Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width)));
}; };
const stop = () => { const stop = () => {
@ -313,10 +314,10 @@ export default function MusicBar() {
{/* 볼륨 — 모바일 포함 전체 표시 */} {/* 볼륨 — 모바일 포함 전체 표시 */}
<button <button
onClick={toggleMute} onClick={toggleMute}
aria-label={muted ? t.playerPlay : t.playerPause} aria-label={muted ? t.playerUnmute : t.playerMute}
className="p-1 text-white/45 hover:text-white/80 transition-colors shrink-0" className="p-1 text-white/45 hover:text-white/80 transition-colors shrink-0"
> >
<IconVolume level={displayVolume} muted={muted} /> <IconVolume level={displayVolume} />
</button> </button>
<div <div
className="w-16 md:w-20 h-[3px] bg-white/20 rounded-full cursor-pointer shrink-0 relative" className="w-16 md:w-20 h-[3px] bg-white/20 rounded-full cursor-pointer shrink-0 relative"

View File

@ -122,6 +122,8 @@ type Dict = {
playerNext: string; playerNext: string;
playerPrev: string; playerPrev: string;
playerClose: string; playerClose: string;
playerMute: string;
playerUnmute: string;
}; };
export const DICT: Record<Lang, Dict> = { export const DICT: Record<Lang, Dict> = {
@ -212,6 +214,8 @@ export const DICT: Record<Lang, Dict> = {
playerNext: "다음 곡", playerNext: "다음 곡",
playerPrev: "이전 곡", playerPrev: "이전 곡",
playerClose: "플레이어 닫기", playerClose: "플레이어 닫기",
playerMute: "음소거",
playerUnmute: "음소거 해제",
}, },
en: { en: {
heroPill: "AI-powered Global Football Festival 2026 match predictions", heroPill: "AI-powered Global Football Festival 2026 match predictions",
@ -300,6 +304,8 @@ export const DICT: Record<Lang, Dict> = {
playerNext: "Next track", playerNext: "Next track",
playerPrev: "Previous track", playerPrev: "Previous track",
playerClose: "Close player", playerClose: "Close player",
playerMute: "Mute",
playerUnmute: "Unmute",
}, },
}; };

View File

@ -55,8 +55,12 @@ const PlayerContext = createContext<PlayerContextValue | null>(null);
export function PlayerProvider({ children }: { children: ReactNode }) { export function PlayerProvider({ children }: { children: ReactNode }) {
const audioRef = useRef<HTMLAudioElement | null>(null); const audioRef = useRef<HTMLAudioElement | null>(null);
const currentSrcRef = useRef<string>("");
const autoPlayRef = useRef(false);
const isPlayingRef = useRef(false);
const [playlist, setPlaylistState] = useState<Track[]>([]); const [playlist, setPlaylistState] = useState<Track[]>([]);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [loadKey, setLoadKey] = useState(0);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
@ -80,18 +84,12 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
setProgress(audio.duration ? audio.currentTime / audio.duration : 0); setProgress(audio.duration ? audio.currentTime / audio.duration : 0);
}; };
const onLoaded = () => setDuration(audio.duration || 0); const onLoaded = () => setDuration(audio.duration || 0);
const onEnded = () => const onPlay = () => { setIsPlaying(true); isPlayingRef.current = true; };
setCurrentIndex((prev) => const onPause = () => { setIsPlaying(false); isPlayingRef.current = false; };
// playlist.length는 클로저 문제 방지를 위해 ref 대신 함수형 업데이트 외부에서 읽음
prev + 1 // ended 핸들러에서는 playlist ref를 통해 처리
);
const onPlay = () => setIsPlaying(true);
const onPause = () => setIsPlaying(false);
audio.addEventListener("timeupdate", onTimeUpdate); audio.addEventListener("timeupdate", onTimeUpdate);
audio.addEventListener("loadedmetadata", onLoaded); audio.addEventListener("loadedmetadata", onLoaded);
audio.addEventListener("durationchange", onLoaded); audio.addEventListener("durationchange", onLoaded);
audio.addEventListener("ended", onEnded);
audio.addEventListener("play", onPlay); audio.addEventListener("play", onPlay);
audio.addEventListener("pause", onPause); audio.addEventListener("pause", onPause);
@ -100,7 +98,6 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
audio.removeEventListener("timeupdate", onTimeUpdate); audio.removeEventListener("timeupdate", onTimeUpdate);
audio.removeEventListener("loadedmetadata", onLoaded); audio.removeEventListener("loadedmetadata", onLoaded);
audio.removeEventListener("durationchange", onLoaded); audio.removeEventListener("durationchange", onLoaded);
audio.removeEventListener("ended", onEnded);
audio.removeEventListener("play", onPlay); audio.removeEventListener("play", onPlay);
audio.removeEventListener("pause", onPause); audio.removeEventListener("pause", onPause);
audioRef.current = null; audioRef.current = null;
@ -115,6 +112,7 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio) return; if (!audio) return;
const onEnded = () => { const onEnded = () => {
autoPlayRef.current = true;
setCurrentIndex((prev) => setCurrentIndex((prev) =>
playlistLenRef.current > 0 ? (prev + 1) % playlistLenRef.current : 0 playlistLenRef.current > 0 ? (prev + 1) % playlistLenRef.current : 0
); );
@ -127,15 +125,17 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio || !currentTrack) return; if (!audio || !currentTrack) return;
const wasPlaying = isPlaying; const shouldPlay = isPlayingRef.current || autoPlayRef.current;
autoPlayRef.current = false;
currentSrcRef.current = currentTrack.src;
audio.src = currentTrack.src; audio.src = currentTrack.src;
audio.load(); audio.load();
setProgress(0); setProgress(0);
setCurrentTime(0); setCurrentTime(0);
setDuration(0); setDuration(0);
if (wasPlaying) audio.play().catch(() => {}); if (shouldPlay) audio.play().catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentIndex, currentTrack?.src]); }, [currentIndex, currentTrack?.src, loadKey]);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Actions // Actions
@ -144,7 +144,11 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
const play = useCallback(() => { const play = useCallback(() => {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio) return; if (!audio) return;
if (!audio.src && currentTrack) { audio.src = currentTrack.src; audio.load(); } if (currentTrack && currentSrcRef.current !== currentTrack.src) {
currentSrcRef.current = currentTrack.src;
audio.src = currentTrack.src;
audio.load();
}
audio.play().catch(() => {}); audio.play().catch(() => {});
}, [currentTrack]); }, [currentTrack]);
@ -156,7 +160,11 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
} else { } else {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio) return; if (!audio) return;
if (!audio.src && currentTrack) { audio.src = currentTrack.src; audio.load(); } if (currentTrack && currentSrcRef.current !== currentTrack.src) {
currentSrcRef.current = currentTrack.src;
audio.src = currentTrack.src;
audio.load();
}
audio.play().catch(() => {}); audio.play().catch(() => {});
} }
}, [isPlaying, currentTrack]); }, [isPlaying, currentTrack]);
@ -205,13 +213,20 @@ export function PlayerProvider({ children }: { children: ReactNode }) {
// 경로 변경 시 새 플레이리스트 로드 // 경로 변경 시 새 플레이리스트 로드
const loadPlaylist = useCallback((tracks: Track[]) => { const loadPlaylist = useCallback((tracks: Track[]) => {
const audio = audioRef.current; const audio = audioRef.current;
if (audio) { audio.pause(); audio.src = ""; } if (audio) {
setPlaylistState(tracks); audio.pause();
audio.removeAttribute("src");
}
currentSrcRef.current = "";
autoPlayRef.current = true;
setPlaylistState([...tracks]);
setLoadKey((k) => k + 1);
setCurrentIndex(0); setCurrentIndex(0);
setProgress(0); setProgress(0);
setCurrentTime(0); setCurrentTime(0);
setDuration(0); setDuration(0);
setIsPlaying(false); setIsPlaying(false);
isPlayingRef.current = false;
}, []); }, []);
const value: PlayerContextValue = { const value: PlayerContextValue = {