From 390e06113d464290c33af0b08deb8ac3b0dbf001 Mon Sep 17 00:00:00 2001 From: jwkim Date: Tue, 25 Aug 2026 09:53:53 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=91=EC=9B=90=EA=B0=80=20=EC=9E=AC?= =?UTF-8?q?=EC=83=9D=EC=9D=84=20=ED=8C=80=EB=B3=84=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일정/메인 자동 로드 제거 — 상세 페이지 대결 카드의 팀 로고 아래 "응원가 듣기" 버튼으로 각 팀 곡을 개별 재생 (재생 중 일시정지/이어듣기 토글) - MusicBar 는 버튼이 실은 플레이리스트도 표시하도록 노출 조건 정리 - 두산 상설 응원가는 두산 버튼 트랙 뒤에 이어붙임 Co-Authored-By: Claude Fable 5 --- frontend/src/components/MatchupHUD.tsx | 69 ++++++++++++++++++++++++-- frontend/src/components/MusicBar.tsx | 21 +++----- frontend/src/lib/playlist.ts | 30 +++++------ 3 files changed, 88 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/MatchupHUD.tsx b/frontend/src/components/MatchupHUD.tsx index 56a85b7..03692cf 100644 --- a/frontend/src/components/MatchupHUD.tsx +++ b/frontend/src/components/MatchupHUD.tsx @@ -1,11 +1,64 @@ +import { useEffect, useState } from "react"; import type { Match, TeamStanding } from "@/lib/types"; import { kickoffDisplay } from "@/lib/format"; import { type Lang, dict, teamShort, roundLabel as tRound } from "@/lib/i18n"; +import { getTeamSongTracks, type Track } from "@/lib/playlist"; +import { usePlayer } from "@/lib/usePlayer"; import BallIcon from "./BallIcon"; import { FormBadges } from "./ScheduleBoard"; import TeamFlag from "./TeamFlag"; import WaveStrip from "./WaveStrip"; +// 팀별 '응원가 듣기' 버튼 — 오늘의 응원가(Suno 자동 생성)를 플레이어에 싣는다. +// 이 팀 곡이 재생 중이면 일시정지/이어듣기 토글. 곡이 없으면 렌더 안 함. +function CheerSongButton({ + matchId, + teamCode, + lang, +}: { + matchId: string; + teamCode: string; + lang: Lang; +}) { + const { currentTrack, isPlaying, toggle, loadPlaylist, open } = usePlayer(); + const [tracks, setTracks] = useState([]); + useEffect(() => { + let alive = true; + getTeamSongTracks(matchId, teamCode).then((ts) => alive && setTracks(ts)); + return () => { + alive = false; + }; + }, [matchId, teamCode]); + + if (tracks.length === 0) return null; + const active = !!currentTrack && tracks.some((t) => t.src === currentTrack.src); + const label = active + ? isPlaying + ? lang === "en" ? "⏸ Pause" : "⏸ 일시정지" + : lang === "en" ? "▶ Resume" : "▶ 이어 듣기" + : lang === "en" ? "♪ Cheer song" : "♪ 응원가 듣기"; + + return ( + + ); +} + // 순위 캐시 → "3위 · 52승37패 (0.584)" 한 줄 (야구) function standingLine( st: { rank?: number | null; w?: number; l?: number; wra?: string | number } | null | undefined, @@ -103,9 +156,14 @@ export default function MatchupHUD({ match, lang = "ko" }: { match: Match; lang? - {/* 국기/로고 + 라이트닝 VS (종료 시 최종 스코어) */} + {/* 국기/로고 + 라이트닝 VS (종료 시 최종 스코어) + 팀별 응원가 버튼(KBO) */}
- +
+ + {match.league === "kbo" && ( + + )} +
{finished && match.result ? ( @@ -123,7 +181,12 @@ export default function MatchupHUD({ match, lang = "ko" }: { match: Match; lang? )}
- +
+ + {match.league === "kbo" && ( + + )} +
{/* 야구 정보 패널: 선발 매치업 · 순위 · 시즌 상대전적 (캐시 있을 때만) */} diff --git a/frontend/src/components/MusicBar.tsx b/frontend/src/components/MusicBar.tsx index 5b58372..7950fd4 100644 --- a/frontend/src/components/MusicBar.tsx +++ b/frontend/src/components/MusicBar.tsx @@ -1,9 +1,9 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useLocation } from "react-router-dom"; import { usePlayer } from "@/lib/usePlayer"; import { useLang } from "@/lib/useLang"; import { dict } from "@/lib/i18n"; -import { getTracksForPathAsync, type Track } from "@/lib/playlist"; +import { getTracksForPath } from "@/lib/playlist"; // --------------------------------------------------------------------------- // 시간 포맷 @@ -145,21 +145,14 @@ export default function MusicBar() { loadPlaylist, } = usePlayer(); - // 경로가 바뀌면 해당 경로의 플레이리스트 로드 (오늘의 응원가 API 포함), 없으면 초기화 - const [tracks, setTracks] = useState([]); + // 경로가 바뀌면 해당 경로의 자동 플레이리스트 로드(월드컵 특설), 그 외는 초기화. + // KBO 응원가는 상세 페이지 '응원가 듣기' 버튼이 loadPlaylist 로 직접 채운다. useEffect(() => { - let alive = true; - getTracksForPathAsync(pathname).then((ts) => { - if (!alive) return; - setTracks(ts); - loadPlaylist(ts); - }); - return () => { - alive = false; - }; + loadPlaylist(getTracksForPath(pathname)); }, [pathname, loadPlaylist]); - if (!hasTrack || tracks.length === 0) return null; + // 자동이든 버튼이든 플레이리스트가 실리면 바 노출, 비면 숨김 + if (!hasTrack) return null; const handleProgressClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); diff --git a/frontend/src/lib/playlist.ts b/frontend/src/lib/playlist.ts index cf39f55..e80b568 100644 --- a/frontend/src/lib/playlist.ts +++ b/frontend/src/lib/playlist.ts @@ -35,13 +35,11 @@ const DOOSAN_TRACKS: Track[] = [ { title: "Bearz Day", artist: "aio2o", src: "/audio/Bearz Day.mp3", cover: "/thumbnail/Bearz Day.webp" }, ]; -// 경로 → 트랙 목록. 정확일치(PATH_PLAYLISTS) 우선, 그 외 KBO 두산 경기 -// (match_id 'KBO_{팀A}_{팀B}_{YYYYMMDD}' 에 OB 포함)면 두산 플레이리스트. +// 경로 → 자동 로드 트랙 목록 — 정확일치(PATH_PLAYLISTS, 월드컵 특설)만. +// KBO 오늘의 응원가는 자동재생하지 않고 상세 페이지의 팀별 '응원가 듣기' +// 버튼(getTeamSongTracks)으로만 재생한다. export function getTracksForPath(pathname: string): Track[] { - const exact = PATH_PLAYLISTS[pathname]; - if (exact) return exact; - if (/^\/match\/KBO_(OB_[A-Z]+|[A-Z]+_OB)_\d{8}$/.test(pathname)) return DOOSAN_TRACKS; - return []; + return PATH_PLAYLISTS[pathname] ?? []; } // ── 오늘의 응원가 (백엔드 Suno 자동 생성) ───────────────────── @@ -76,14 +74,16 @@ function songToTracks(s: SongOut): Track[] { })); } -// 경로 → 트랙 목록 (동적 응원가 포함). 경기 상세는 그 경기 두 팀의 곡, -// 메인("/")은 오늘 전 경기 곡. 동적 곡이 없으면 기존 정적 목록 그대로. -export async function getTracksForPathAsync(pathname: string): Promise { - const staticTracks = getTracksForPath(pathname); - const m = pathname.match(/^\/match\/(KBO_.+)$/); - if (!m && pathname !== "/") return staticTracks; +// 특정 경기·팀의 오늘의 응원가 트랙 — 상세 페이지 '응원가 듣기' 버튼용. +// 두산(OB)은 상설 응원가를 뒤에 이어붙인다. 곡이 없으면 빈 배열. +export async function getTeamSongTracks( + matchId: string, + teamCode: string, +): Promise { const songs = await fetchSongsCached(); - const picked = m ? songs.filter((s) => s.matchId === m[1]) : songs; - const dyn = picked.flatMap(songToTracks); - return dyn.length ? [...dyn, ...staticTracks] : staticTracks; + const dyn = songs + .filter((s) => s.matchId === matchId && s.teamCode === teamCode) + .flatMap(songToTracks); + const fixed = teamCode === "OB" && matchId.startsWith("KBO_") ? DOOSAN_TRACKS : []; + return [...dyn, ...fixed]; }