fix: 음악자동재생 + 버그수정
parent
2988a59749
commit
0e5a63a28b
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue