From 0e5a63a28bf23b2e51a26d11799b1ecb2af6e8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Thu, 18 Jun 2026 13:08:51 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=8C=EC=95=85=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=20+=20=EB=B2=84=EA=B7=B8=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MusicBar.tsx | 15 ++++----- frontend/src/lib/i18n.ts | 6 ++++ frontend/src/lib/usePlayer.tsx | 47 ++++++++++++++++++---------- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/MusicBar.tsx b/frontend/src/components/MusicBar.tsx index 0c62cd7..9c25ae1 100644 --- a/frontend/src/components/MusicBar.tsx +++ b/frontend/src/components/MusicBar.tsx @@ -53,7 +53,7 @@ function IconMusic() { ); } -function IconVolume({ level }: { level: number; muted: boolean }) { +function IconVolume({ level }: { level: number }) { if (level === 0) { return ( @@ -145,10 +145,10 @@ export default function MusicBar() { loadPlaylist, } = usePlayer(); - // 경로가 바뀌면 해당 경로의 플레이리스트 로드 + // 경로가 바뀌면 해당 경로의 플레이리스트 로드, 없으면 초기화 useEffect(() => { const tracks = PATH_PLAYLISTS[pathname]; - if (tracks) loadPlaylist(tracks); + loadPlaylist(tracks ?? []); }, [pathname, loadPlaylist]); if (!hasTrack) return null; @@ -171,9 +171,10 @@ export default function MusicBar() { }; const handleVolumeDrag = (e: React.PointerEvent) => { - e.currentTarget.setPointerCapture(e.pointerId); + const el = e.currentTarget; + el.setPointerCapture(e.pointerId); 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))); }; const stop = () => { @@ -313,10 +314,10 @@ export default function MusicBar() { {/* 볼륨 — 모바일 포함 전체 표시 */}
= { @@ -212,6 +214,8 @@ export const DICT: Record = { playerNext: "다음 곡", playerPrev: "이전 곡", playerClose: "플레이어 닫기", + playerMute: "음소거", + playerUnmute: "음소거 해제", }, en: { heroPill: "AI-powered Global Football Festival 2026 match predictions", @@ -300,6 +304,8 @@ export const DICT: Record = { playerNext: "Next track", playerPrev: "Previous track", playerClose: "Close player", + playerMute: "Mute", + playerUnmute: "Unmute", }, }; diff --git a/frontend/src/lib/usePlayer.tsx b/frontend/src/lib/usePlayer.tsx index 1afc52e..ff63fc0 100644 --- a/frontend/src/lib/usePlayer.tsx +++ b/frontend/src/lib/usePlayer.tsx @@ -55,8 +55,12 @@ const PlayerContext = createContext(null); export function PlayerProvider({ children }: { children: ReactNode }) { const audioRef = useRef(null); + const currentSrcRef = useRef(""); + const autoPlayRef = useRef(false); + const isPlayingRef = useRef(false); const [playlist, setPlaylistState] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); + const [loadKey, setLoadKey] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = 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); }; const onLoaded = () => setDuration(audio.duration || 0); - const onEnded = () => - setCurrentIndex((prev) => - // playlist.length는 클로저 문제 방지를 위해 ref 대신 함수형 업데이트 외부에서 읽음 - prev + 1 // ended 핸들러에서는 playlist ref를 통해 처리 - ); - const onPlay = () => setIsPlaying(true); - const onPause = () => setIsPlaying(false); + const onPlay = () => { setIsPlaying(true); isPlayingRef.current = true; }; + const onPause = () => { setIsPlaying(false); isPlayingRef.current = false; }; audio.addEventListener("timeupdate", onTimeUpdate); audio.addEventListener("loadedmetadata", onLoaded); audio.addEventListener("durationchange", onLoaded); - audio.addEventListener("ended", onEnded); audio.addEventListener("play", onPlay); audio.addEventListener("pause", onPause); @@ -100,7 +98,6 @@ export function PlayerProvider({ children }: { children: ReactNode }) { audio.removeEventListener("timeupdate", onTimeUpdate); audio.removeEventListener("loadedmetadata", onLoaded); audio.removeEventListener("durationchange", onLoaded); - audio.removeEventListener("ended", onEnded); audio.removeEventListener("play", onPlay); audio.removeEventListener("pause", onPause); audioRef.current = null; @@ -115,6 +112,7 @@ export function PlayerProvider({ children }: { children: ReactNode }) { const audio = audioRef.current; if (!audio) return; const onEnded = () => { + autoPlayRef.current = true; setCurrentIndex((prev) => playlistLenRef.current > 0 ? (prev + 1) % playlistLenRef.current : 0 ); @@ -127,15 +125,17 @@ export function PlayerProvider({ children }: { children: ReactNode }) { useEffect(() => { const audio = audioRef.current; 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.load(); setProgress(0); setCurrentTime(0); setDuration(0); - if (wasPlaying) audio.play().catch(() => {}); + if (shouldPlay) audio.play().catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentIndex, currentTrack?.src]); + }, [currentIndex, currentTrack?.src, loadKey]); // --------------------------------------------------------------------------- // Actions @@ -144,7 +144,11 @@ export function PlayerProvider({ children }: { children: ReactNode }) { const play = useCallback(() => { const audio = audioRef.current; 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(() => {}); }, [currentTrack]); @@ -156,7 +160,11 @@ export function PlayerProvider({ children }: { children: ReactNode }) { } else { const audio = audioRef.current; 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(() => {}); } }, [isPlaying, currentTrack]); @@ -205,13 +213,20 @@ export function PlayerProvider({ children }: { children: ReactNode }) { // 경로 변경 시 새 플레이리스트 로드 const loadPlaylist = useCallback((tracks: Track[]) => { const audio = audioRef.current; - if (audio) { audio.pause(); audio.src = ""; } - setPlaylistState(tracks); + if (audio) { + audio.pause(); + audio.removeAttribute("src"); + } + currentSrcRef.current = ""; + autoPlayRef.current = true; + setPlaylistState([...tracks]); + setLoadKey((k) => k + 1); setCurrentIndex(0); setProgress(0); setCurrentTime(0); setDuration(0); setIsPlaying(false); + isPlayingRef.current = false; }, []); const value: PlayerContextValue = {