Skip to content

feat :: 음악 신청 api 연결#87

Open
soriii1 wants to merge 1 commit into
mainfrom
feat#86
Open

feat :: 음악 신청 api 연결#87
soriii1 wants to merge 1 commit into
mainfrom
feat#86

Conversation

@soriii1
Copy link
Copy Markdown
Contributor

@soriii1 soriii1 commented Dec 28, 2025

#86

Summary by CodeRabbit

  • New Features

    • Integrated music queue and live audio player in the timer view with track metadata display
    • Added automatic queue refresh and sequential track playback
    • Enhanced music submission with clearer YouTube URL input prompts
  • Bug Fixes

    • Improved error handling for music submission failures with detailed user-facing alerts

✏️ Tip: You can customize this high-level summary in your review settings.

@soriii1 soriii1 linked an issue Dec 28, 2025 that may be closed by this pull request
1 task
@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 28, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
fe Ready Ready Preview, Comment Dec 28, 2025 6:45pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 28, 2025

📝 Walkthrough

Walkthrough

The changes introduce YouTube URL-based music functionality across the application: MusicModal UI updated for YouTube URL input, StudentHome refactored to POST music requests with YouTube URLs to a new std/music/request endpoint with expanded error handling, and TimerHome extended with music queue display and HTML5 audio player integration featuring periodic queue refresh and automatic playback.

Changes

Cohort / File(s) Summary
Music Modal UI
src/components/MusicModal.jsx
Updated UI strings: title, placeholder text, and example hint to reference YouTube URLs instead of BGM hacking prompt. Behavior unchanged.
Music Submission Logic
src/pages/StudentHome.jsx
Renamed handleMusicSubmit parameter from music to youtubeUrl; reworked API call to POST to std/music/request with payload { url: youtubeUrl } and Authorization header; added debug logging; expanded error handling to cover PAYMENT_REQUIRED, OUT_OF_STOCK, NON_EXIST_ITEM, BAD_REQUEST, PERMISSION_DENIED, NON_EXIST_TEAM, and 403/404 status codes; changed error flow to rethrow and prevent automatic modal closure.
Music Playback & Queue
src/pages/TimerHome.jsx
Replaced static Timer page with integrated music queue UI and HTML5 audio player; added state management for musicQueue, nowPlaying, and loading; implemented fetchMusicQueue (10-second refresh cycle), handlePlay (stream music with metadata extraction), and handleEnded (cleanup and auto-play next); added styled components for queue, now-playing block, and audio controls; enhanced playback error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MusicModal
    participant StudentHome
    participant API
    participant TimerHome
    participant AudioPlayer

    User->>MusicModal: Submit YouTube URL
    MusicModal->>StudentHome: handleMusicSubmit(youtubeUrl)
    StudentHome->>API: POST /std/music/request<br/>{url: youtubeUrl}<br/>(with Authorization)
    
    rect rgb(200, 220, 255)
    Note over API: Process music request
    alt Success
        API-->>StudentHome: 200 + Response
        StudentHome->>StudentHome: Log response, store data
        StudentHome-->>MusicModal: Success (handled by modal)
    else Error
        API-->>StudentHome: Error response<br/>(4xx/5xx or custom code)
        StudentHome->>StudentHome: Map error to category<br/>& compose message
        StudentHome-->>MusicModal: Alert user & rethrow
    end
    end
    
    User->>TimerHome: View timer page
    TimerHome->>API: GET /music/queue
    API-->>TimerHome: musicQueue list
    
    loop Every 10 seconds
        TimerHome->>API: Refresh queue
        API-->>TimerHome: Updated queue
    end
    
    User->>TimerHome: Click play on music item
    TimerHome->>API: GET /music/stream?id={itemId}<br/>(with Authorization)
    API-->>TimerHome: Audio stream + metadata headers
    TimerHome->>TimerHome: Extract metadata,<br/>create blob URL
    TimerHome->>AudioPlayer: Update nowPlaying & audio src
    AudioPlayer->>User: ▶️ Play audio
    
    AudioPlayer->>TimerHome: onEnded event
    TimerHome->>TimerHome: Clean up previous URL<br/>clear nowPlaying
    TimerHome->>API: Refresh queue
    alt Next track available
        TimerHome->>TimerHome: Auto-play next track
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • HARAM-ARA/FE#83: Introduces the main YouTube URL-based music submission flow with new std/music/request endpoint integration and comprehensive error handling that ties together MusicModal UI updates and StudentHome API logic changes.

Poem

🎵 A hopping refactor brings songs to light,
YouTube URLs flowing left and right,
Queues refresh every ten-second beat,
Players dance as audio tracks complete! 🐰
Modal whispers, API sings, lo-fi dreams take flight! 🎶

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of this PR: connecting the music request API. All three modified files (MusicModal, StudentHome, TimerHome) are focused on implementing and integrating music submission and playback functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat#86

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/pages/StudentHome.jsx (3)

250-251: Consider removing debug logs before production.

These console.log statements are useful for development but should be removed or converted to conditional logging before deploying to production to avoid leaking potentially sensitive information (like user-submitted URLs).

🔎 Suggested fix
     const handleMusicSubmit = async (youtubeUrl) => {
         try {
             const token = localStorage.getItem('auth_token');

-            console.log("===== 음악 신청 시작 =====");
-            console.log("YouTube URL:", youtubeUrl);
-
             // 1단계: 상점에서 음악 신청 쿠폰 찾기 (ID: 998)

290-304: Approve API integration; consider removing debug logs.

The API call structure is correct with proper authorization headers. The success messaging is handled by MusicModal as noted in the comment, which aligns with the component's design.

The console.log on lines 302-303 should also be removed for production consistency.


338-340: String-based error filtering is fragile.

The condition relies on checking if errorMsg.includes() specific Korean strings. This can break if error messages are changed or translated. Consider using a local flag or error code instead.

🔎 Suggested approach
+    let alertShown = false;
+
     const handleMusicSubmit = async (youtubeUrl) => {
         try {
             // ... existing code ...

             if (!musicItem) {
                 alert("음악 신청 상품을 찾을 수 없습니다.");
+                alertShown = true;
                 throw new Error("음악 신청 상품을 찾을 수 없습니다.");
             }
             // ... similarly for other early throws ...
         } catch (error) {
             // ... existing error handling ...
-            } else if (!errorMsg.includes("크레딧 부족") && !errorMsg.includes("찾을 수 없습니다")) {
+            } else if (!alertShown) {
                 alert(`음악 신청에 실패했습니다: ${errorMsg}`);
             }
+            alertShown = false;
             throw error;
         }
     };
src/pages/TimerHome.jsx (1)

207-226: Music queue list is not rendered.

The styled components QueueList, QueueItem, and PlayButton (lines 31-89) are defined but never used in the render output. Currently, only the "Now Playing" section is displayed. If the queue display is intentional for a future iteration, consider adding a TODO comment; otherwise, the unused components should be removed or the queue should be rendered.

Is this intentional? Would you like me to generate the missing queue display implementation?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 162df04 and ed0f559.

📒 Files selected for processing (3)
  • src/components/MusicModal.jsx
  • src/pages/StudentHome.jsx
  • src/pages/TimerHome.jsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/MusicModal.jsx (1)
src/components/AnnouncementModal.jsx (5)
  • Title (64-73)
  • Example (75-85)
  • InputBox (88-111)
  • inputValue (180-180)
  • handleKeyDown (220-225)
src/pages/StudentHome.jsx (4)
src/lib/customAxios.js (1)
  • token (34-34)
src/lib/auth.js (1)
  • response (5-5)
src/pages/TeamSpace.jsx (1)
  • error (290-290)
src/pages/MyTeam.jsx (1)
  • error (218-218)
src/pages/TimerHome.jsx (1)
src/components/Timer.jsx (1)
  • Timer (128-269)
🔇 Additional comments (3)
src/components/MusicModal.jsx (1)

243-249: LGTM!

The UI text updates are clear and consistent. The title, example, and placeholder all correctly guide users to input a YouTube URL. Validation is appropriately delegated to the submission handler.

src/pages/TimerHome.jsx (2)

113-116: LGTM on state and ref initialization.

State variables and audio ref are properly initialized for managing the music queue and playback.


129-134: LGTM!

The interval is properly cleaned up on unmount, preventing memory leaks. The 10-second polling interval is reasonable for keeping the queue updated.

Comment thread src/pages/TimerHome.jsx
Comment on lines +137 to +189
const handlePlay = async (musicId) => {
try {
setLoading(true);
const token = localStorage.getItem('auth_token');

const url = musicId
? `${import.meta.env.VITE_API_URL}tch/music/stream/${musicId}`
: `${import.meta.env.VITE_API_URL}tch/music/stream`;

const response = await customaxios.get(url, {
headers: {
'Authorization': `Bearer ${token}`
},
responseType: 'blob'
});

// 헤더에서 메타데이터 추출
const title = decodeURIComponent(response.headers['x-music-title'] || 'Unknown');
const requester = decodeURIComponent(response.headers['x-music-requester'] || 'Unknown');
const team = decodeURIComponent(response.headers['x-music-team'] || 'Unknown');

// Blob URL 생성
const audioBlob = new Blob([response.data], { type: 'audio/webm' });
const audioUrl = URL.createObjectURL(audioBlob);

setNowPlaying({
id: musicId,
title,
requester,
team,
url: audioUrl
});

// 오디오 재생
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play();
}

// 큐 업데이트
await fetchMusicQueue();

} catch (error) {
console.error("음악 재생 실패:", error);
if (error.response?.data?.error === "NOT_FOUND") {
alert("재생할 음악이 없습니다.");
} else {
alert("음악 재생에 실패했습니다.");
}
} finally {
setLoading(false);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential blob URL memory leak.

When handlePlay is called while another track is playing (or on unmount), the previous blob URL is not revoked. This can lead to memory leaks.

🔎 Suggested fix
     const handlePlay = async (musicId) => {
         try {
             setLoading(true);
+            
+            // Revoke previous blob URL if exists
+            if (nowPlaying?.url) {
+                URL.revokeObjectURL(nowPlaying.url);
+            }
+            
             const token = localStorage.getItem('auth_token');
             // ... rest of the code

Also add cleanup on unmount:

useEffect(() => {
    return () => {
        if (nowPlaying?.url) {
            URL.revokeObjectURL(nowPlaying.url);
        }
    };
}, [nowPlaying?.url]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePlay = async (musicId) => {
try {
setLoading(true);
const token = localStorage.getItem('auth_token');
const url = musicId
? `${import.meta.env.VITE_API_URL}tch/music/stream/${musicId}`
: `${import.meta.env.VITE_API_URL}tch/music/stream`;
const response = await customaxios.get(url, {
headers: {
'Authorization': `Bearer ${token}`
},
responseType: 'blob'
});
// 헤더에서 메타데이터 추출
const title = decodeURIComponent(response.headers['x-music-title'] || 'Unknown');
const requester = decodeURIComponent(response.headers['x-music-requester'] || 'Unknown');
const team = decodeURIComponent(response.headers['x-music-team'] || 'Unknown');
// Blob URL 생성
const audioBlob = new Blob([response.data], { type: 'audio/webm' });
const audioUrl = URL.createObjectURL(audioBlob);
setNowPlaying({
id: musicId,
title,
requester,
team,
url: audioUrl
});
// 오디오 재생
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play();
}
// 큐 업데이트
await fetchMusicQueue();
} catch (error) {
console.error("음악 재생 실패:", error);
if (error.response?.data?.error === "NOT_FOUND") {
alert("재생할 음악이 없습니다.");
} else {
alert("음악 재생에 실패했습니다.");
}
} finally {
setLoading(false);
}
};
const handlePlay = async (musicId) => {
try {
setLoading(true);
// Revoke previous blob URL if exists
if (nowPlaying?.url) {
URL.revokeObjectURL(nowPlaying.url);
}
const token = localStorage.getItem('auth_token');
const url = musicId
? `${import.meta.env.VITE_API_URL}tch/music/stream/${musicId}`
: `${import.meta.env.VITE_API_URL}tch/music/stream`;
const response = await customaxios.get(url, {
headers: {
'Authorization': `Bearer ${token}`
},
responseType: 'blob'
});
// 헤더에서 메타데이터 추출
const title = decodeURIComponent(response.headers['x-music-title'] || 'Unknown');
const requester = decodeURIComponent(response.headers['x-music-requester'] || 'Unknown');
const team = decodeURIComponent(response.headers['x-music-team'] || 'Unknown');
// Blob URL 생성
const audioBlob = new Blob([response.data], { type: 'audio/webm' });
const audioUrl = URL.createObjectURL(audioBlob);
setNowPlaying({
id: musicId,
title,
requester,
team,
url: audioUrl
});
// 오디오 재생
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play();
}
// 큐 업데이트
await fetchMusicQueue();
} catch (error) {
console.error("음악 재생 실패:", error);
if (error.response?.data?.error === "NOT_FOUND") {
alert("재생할 음악이 없습니다.");
} else {
alert("음악 재생에 실패했습니다.");
}
} finally {
setLoading(false);
}
};
🤖 Prompt for AI Agents
In src/pages/TimerHome.jsx around lines 137 to 189, the handler creates blob
URLs for streamed audio but never revokes the previous URL, causing a memory
leak; update handlePlay to revoke the existing nowPlaying.url (if any) before
creating and setting a new audioUrl, and add a useEffect cleanup that revokes
the current nowPlaying.url on unmount (or when nowPlaying.url changes) so any
leftover object URLs are released.

Comment thread src/pages/TimerHome.jsx
Comment on lines +191 to +202
// 음악 종료 시 다음 곡 자동 재생
const handleEnded = () => {
if (nowPlaying?.url) {
URL.revokeObjectURL(nowPlaying.url);
}
setNowPlaying(null);

// 큐에 다음 곡이 있으면 자동 재생
if (musicQueue.length > 0) {
handlePlay();
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stale closure bug: musicQueue may be outdated when handleEnded executes.

The handleEnded callback captures musicQueue from the render when it was attached to the <audio> element. By the time the audio ends, the queue may have been refreshed multiple times, but handleEnded still references the old array.

🔎 Suggested fix using a ref
+    const musicQueueRef = useRef([]);
+
+    // Keep ref in sync with state
+    useEffect(() => {
+        musicQueueRef.current = musicQueue;
+    }, [musicQueue]);

     const handleEnded = () => {
         if (nowPlaying?.url) {
             URL.revokeObjectURL(nowPlaying.url);
         }
         setNowPlaying(null);

         // 큐에 다음 곡이 있으면 자동 재생
-        if (musicQueue.length > 0) {
+        if (musicQueueRef.current.length > 0) {
             handlePlay();
         }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 음악 종료 시 다음 곡 자동 재생
const handleEnded = () => {
if (nowPlaying?.url) {
URL.revokeObjectURL(nowPlaying.url);
}
setNowPlaying(null);
// 큐에 다음 곡이 있으면 자동 재생
if (musicQueue.length > 0) {
handlePlay();
}
};
const musicQueueRef = useRef([]);
// Keep ref in sync with state
useEffect(() => {
musicQueueRef.current = musicQueue;
}, [musicQueue]);
// 음악 종료 시 다음 곡 자동 재생
const handleEnded = () => {
if (nowPlaying?.url) {
URL.revokeObjectURL(nowPlaying.url);
}
setNowPlaying(null);
// 큐에 다음 곡이 있으면 자동 재생
if (musicQueueRef.current.length > 0) {
handlePlay();
}
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 음악 신청 모달 API 연결

1 participant