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>
);
}
function IconVolume({ level }: { level: number; muted: boolean }) {
function IconVolume({ level }: { level: number }) {
if (level === 0) {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@ -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<HTMLDivElement>) => {
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() {
{/* 볼륨 — 모바일 포함 전체 표시 */}
<button
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"
>
<IconVolume level={displayVolume} muted={muted} />
<IconVolume level={displayVolume} />
</button>
<div
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;
playerPrev: string;
playerClose: string;
playerMute: string;
playerUnmute: string;
};
export const DICT: Record<Lang, Dict> = {
@ -212,6 +214,8 @@ export const DICT: Record<Lang, Dict> = {
playerNext: "다음 곡",
playerPrev: "이전 곡",
playerClose: "플레이어 닫기",
playerMute: "음소거",
playerUnmute: "음소거 해제",
},
en: {
heroPill: "AI-powered Global Football Festival 2026 match predictions",
@ -300,6 +304,8 @@ export const DICT: Record<Lang, Dict> = {
playerNext: "Next track",
playerPrev: "Previous track",
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 }) {
const audioRef = useRef<HTMLAudioElement | null>(null);
const currentSrcRef = useRef<string>("");
const autoPlayRef = useRef(false);
const isPlayingRef = useRef(false);
const [playlist, setPlaylistState] = useState<Track[]>([]);
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 = {