Conversation
|
@CodiumAI-Agent /improve |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@CodiumAI-Agent /improve |
There was a problem hiding this comment.
Pull request overview
비회원(게스트) 상태에서도 “부를 곡”을 추가/조회/삭제할 수 있도록 상태 저장 방식을 개선하고, 인기곡 페이지를 데이터 조회 컴포넌트 중심으로 리팩토링한 PR입니다. 또한 인증 여부에 따라 일부 React Query 요청을 비활성화하여 불필요한 서버 요청을 줄이려는 변경이 포함되어 있습니다.
Changes:
- 게스트용 “부를 곡” 목록을 zustand persist 기반으로 저장하고, 인증 여부에 따라 서버/로컬 데이터를 분기 처리
- 인기곡 페이지 구조 리팩토링(목록 컴포넌트 내부에서 query 처리 + 수동 refetch UI 추가)
- API 응답 타입(ApiSuccessResponse) 개선 및 일부 query enabled 제어 적용
Reviewed changes
Copilot reviewed 27 out of 29 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | React Query Devtools 버전 변경 반영 |
| apps/web/src/types/apiRoute.ts | ApiSuccessResponse를 조건부 타입으로 변경 |
| apps/web/src/stores/useGuestToSingStore.ts | 게스트 “부를 곡”을 Song 객체 기반으로 persist 저장하도록 구조 변경 |
| apps/web/src/query.tsx | QueryProvider에 ReactQueryDevtools 추가 |
| apps/web/src/queries/tosingSongQuery.ts | 인증 여부에 따른 toSingSong query 분기 및 invalidate 로직 조정 |
| apps/web/src/queries/songThumbQuery.ts | thumb query의 stale/gc 정책 변경 |
| apps/web/src/queries/searchSongQuery.ts | 일부 invalidate 대상에서 recentSingLog 제거 |
| apps/web/src/queries/saveSongQuery.ts | 인증 여부에 따라 query enabled 제어 |
| apps/web/src/queries/likeSongQuery.ts | 인증 여부에 따라 query enabled 제어 |
| apps/web/src/lib/api/tosing.ts | 게스트 tosing 조회용 API 함수 추가 |
| apps/web/src/hooks/useToSingSong.ts | 게스트일 때 drag/delete/move 동작을 로컬 store로 처리하도록 분기 |
| apps/web/src/hooks/useSearchSong.ts | 게스트 tosing 토글 시 Song 객체를 store에 저장하도록 변경 |
| apps/web/src/components/ui/IntervalProgress.tsx | 일정 시간 진행률 UI 신규 추가 |
| apps/web/src/components/StaticLoading.tsx | 불필요한 주석 제거 |
| apps/web/src/components/LoadingOverlay.tsx | 컴포넌트 삭제 |
| apps/web/src/app/tosing/page.tsx | 곡 추가 버튼을 컴포넌트로 분리 |
| apps/web/src/app/tosing/SongList.tsx | useSong → useToSingSong로 훅 변경 |
| apps/web/src/app/tosing/AddSongButton.tsx | 곡 추가 버튼/모달 렌더링 컴포넌트 신규 추가 |
| apps/web/src/app/tosing/AddListModal.tsx | 좋아요/저장 목록 query를 인증 여부로 enabled 제어 |
| apps/web/src/app/search/HomePage.tsx | 게스트 tosing 판정/토글 로직을 객체 기반 store로 변경 |
| apps/web/src/app/popular/page.tsx | PopularRankingList가 자체적으로 데이터를 가져오도록 리팩토링 |
| apps/web/src/app/popular/PopularRankingList.tsx | query 내장 + 수동 refetch UI(아이콘) 추가 |
| apps/web/src/app/info/save/page.tsx | save query 호출부를 인증 여부 인자로 변경 |
| apps/web/src/app/info/like/page.tsx | like query 호출부를 인증 여부 인자로 변경 |
| apps/web/src/app/api/songs/tosing/guest/route.ts | 게스트용 tosing 조회 API route 신규 추가 |
| apps/web/package.json | react-query-devtools 버전 변경 |
| GEMINI.md | 커스텀 규칙 문서 내용 추가 |
| .vscode/settings.json | 파일 삭제 |
| .gitignore | .vscode 무시 추가 및 기존 항목 일부 정리 |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| const interval = setInterval(() => { | ||
| setProgress(prev => { | ||
| const next = prev + (updateInterval / duration) * 100; | ||
| if (next >= 100) { | ||
| onComplete?.(); | ||
| return 100; // Snap to 100, wait for isLoading to become true | ||
| } | ||
| return next; |
There was a problem hiding this comment.
progress가 100 이상이 된 이후에도 interval이 계속 돌면서 onComplete가 매 tick마다 반복 호출될 수 있습니다(현재 state가 100에 고정되어도 next 계산이 계속 >=100). 완료 시점에 interval을 중단하거나, 이미 완료된 경우 onComplete를 한 번만 호출하도록 가드 로직을 추가해주세요.
| return ( | ||
| <QueryClientProvider client={queryClient}> | ||
| {children} | ||
| <ReactQueryDevtools /> | ||
| </QueryClientProvider> |
There was a problem hiding this comment.
ReactQueryDevtools가 항상 렌더링되면 프로덕션 번들/런타임 오버헤드 및 디버그 UI 노출 가능성이 있습니다. 개발 환경에서만 렌더링되도록 조건부로 감싸거나(dynamic import 포함) 빌드 환경 분기 처리를 해주세요.
| <CardTitle className="text-xl">추천 곡 순위</CardTitle> | ||
| {/* <IntervalProgress duration={5000} onComplete={() => refetch()} isLoading={isFetching} /> */} | ||
|
|
||
| <RotateCw onClick={() => refetch()} className="cursor-pointer hover:animate-spin" /> |
There was a problem hiding this comment.
RotateCw 아이콘에 onClick만 달려 있으면 키보드 포커스/Enter 동작, 스크린리더 라벨 등이 제공되지 않아 접근성 이슈가 생깁니다. 버튼 요소로 감싸고 aria-label을 추가(또는 Button 컴포넌트 사용)해서 클릭/키보드 조작 모두 가능하게 해주세요.
| <RotateCw onClick={() => refetch()} className="cursor-pointer hover:animate-spin" /> | |
| <button | |
| type="button" | |
| onClick={() => refetch()} | |
| aria-label="추천 곡 순위 새로고침" | |
| className="cursor-pointer hover:animate-spin" | |
| > | |
| <RotateCw /> | |
| </button> |
| swapGuestToSingSongs: (targetId: string, moveIndex: number) => { | ||
| set(state => { | ||
| const newSongIds = [...state.localToSingSongIds]; | ||
| if ( | ||
| fromIndex < 0 || | ||
| fromIndex >= newSongIds.length || | ||
| toIndex < 0 || | ||
| toIndex >= newSongIds.length | ||
| ) { | ||
| return state; | ||
| } | ||
| const [movedItem] = newSongIds.splice(fromIndex, 1); | ||
| newSongIds.splice(toIndex, 0, movedItem); | ||
| return { localToSingSongIds: newSongIds }; | ||
| const newSongs = [...state.guestToSingSongs]; | ||
| const targetIndex = newSongs.findIndex(item => item.songs.id === targetId); | ||
|
|
||
| if (targetIndex === -1) return state; | ||
|
|
||
| const [movedItem] = newSongs.splice(targetIndex, 1); | ||
| newSongs.splice(moveIndex, 0, movedItem); | ||
|
|
||
| return { guestToSingSongs: newSongs }; |
There was a problem hiding this comment.
swapGuestToSingSongs에서 moveIndex에 대한 범위 체크가 없어 음수/범위 초과 값이 들어오면 예상치 못한 위치로 이동할 수 있습니다(splice의 음수 인덱싱 포함). 0~length 범위로 clamp하거나 잘못된 값이면 state를 그대로 반환하도록 방어 로직을 추가해주세요.
| const supabase = await createClient(); | ||
|
|
||
| const { data, error } = await supabase | ||
| .from('songs') | ||
| .select('*', { count: 'exact' }) |
There was a problem hiding this comment.
GET /songs/tosing/guest에서 songIds가 비어있는 경우에도 .in('id', ids)를 호출하고 있는데, Supabase에서는 빈 배열 in 필터가 에러가 날 수 있습니다. ids.length===0이면 즉시 success:true, data:[]를 반환하도록 처리하고, select의 count 옵션은 사용하지 않으니 제거해서 쿼리 비용도 줄이는 게 좋습니다.
| const supabase = await createClient(); | |
| const { data, error } = await supabase | |
| .from('songs') | |
| .select('*', { count: 'exact' }) | |
| // songIds가 비어있는 경우 Supabase 쿼리를 수행하지 않고 바로 빈 배열 반환 | |
| if (ids.length === 0) { | |
| return NextResponse.json({ success: true, data: [] }); | |
| } | |
| const supabase = await createClient(); | |
| const { data, error } = await supabase | |
| .from('songs') | |
| .select('*') |
| const toSingSongs = data.map((song, index) => ({ | ||
| songs: song, | ||
| order_weight: index, | ||
| })); | ||
|
|
There was a problem hiding this comment.
현재 .in('id', ids)로 가져온 data의 순서는 보장되지 않아서, order_weight를 map index로 두면 클라이언트가 기대한(요청한 ids 순서) 정렬과 달라질 수 있습니다. ids 순서대로 결과를 재정렬한 뒤 order_weight를 부여하거나, 쿼리에서 정렬 기준을 명확히 지정해주세요.
| const toSingSongs = data.map((song, index) => ({ | |
| songs: song, | |
| order_weight: index, | |
| })); | |
| // supabase의 .in('id', ids) 결과 순서는 ids 배열과 같다는 보장이 없기 때문에 | |
| // ids 순서에 맞춰 다시 정렬한 뒤 order_weight를 부여합니다. | |
| const songById = new Map<string, (typeof data)[number]>( | |
| data.map((song) => [String((song as { id: string | number }).id), song]), | |
| ); | |
| const toSingSongs = ids | |
| .map((id, index) => { | |
| const song = songById.get(id); | |
| if (!song) { | |
| // DB에 없는 id는 건너뜁니다. | |
| return null; | |
| } | |
| return { | |
| songs: song, | |
| order_weight: index, | |
| }; | |
| }) | |
| .filter((item): item is { songs: (typeof data)[number]; order_weight: number } => item !== null); |
| export function useToSingSongQuery(isAuthenticated: boolean, guestToSingSongs: ToSingSong[]) { | ||
| return useQuery({ | ||
| queryKey: ['toSingSong'], | ||
| queryKey: isAuthenticated ? ['toSingSong'] : ['toSingSong', 'guest', guestToSingSongs], |
There was a problem hiding this comment.
게스트 상태일 때 queryKey에 guestToSingSongs(객체 배열)를 그대로 넣으면, 키 해시가 커지고(객체 전체 포함) 불필요한 캐시 미스/리렌더가 발생하기 쉽습니다. 게스트 키에는 songId 목록(예: ids 배열)이나 버전/길이처럼 안정적이고 작은 값만 포함하도록 바꾸는 편이 안전합니다.
| // export interface ApiSuccessResponse<T> { | ||
| // success: true; | ||
| // data?: T; | ||
| // hasNext?: boolean; | ||
| // // data: T; 타입 에러 | ||
| // } | ||
|
|
||
| // 조건부 타입 적용 | ||
| // T가 void(비어있음)면 data 필드 자체를 없애고(또는 optional never), T가 있으면 data를 필수(Required)로 구성. |
There was a problem hiding this comment.
ApiSuccessResponse 타입 변경 자체는 좋지만, 이전 interface 정의를 주석으로 남겨두면 파일이 불필요하게 길어지고 유지보수가 어려워집니다. 히스토리는 VCS에 있으니 주석 블록은 제거하고, 필요하면 조건부 타입 설명만 간단히 남겨주세요.
| // export interface ApiSuccessResponse<T> { | |
| // success: true; | |
| // data?: T; | |
| // hasNext?: boolean; | |
| // // data: T; 타입 에러 | |
| // } | |
| // 조건부 타입 적용 | |
| // T가 void(비어있음)면 data 필드 자체를 없애고(또는 optional never), T가 있으면 data를 필수(Required)로 구성. | |
| // ApiSuccessResponse는 제네릭 T에 따라 data 필드 존재 여부를 결정하는 조건부 타입입니다. |
| import { useSongThumbQuery } from '@/queries/songThumbQuery'; | ||
|
|
||
| export default function PopularRankingList() { | ||
| const { data, isPending, refetch, isFetching } = useSongThumbQuery(); |
There was a problem hiding this comment.
Unused variable isFetching.
| const { data, isPending, refetch, isFetching } = useSongThumbQuery(); | |
| const { data, isPending, refetch } = useSongThumbQuery(); |
|
/describe |
|
PR Description updated to latest commit (0cc2adc)
|
There was a problem hiding this comment.
File Walkthrough
| |||||||||||||||||||||
| |||||||||||||||||||||
| |||||||||||||||||||||
| |||||||||||||||||||||
| |||||||||||||||||||||
| |||||||||||||||||||||
| |||||||||||||||||||||
|
|
/review |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
User description
📌 PR 제목
Feat : 게스트 상태에서 부를 곡 추가 변경
📌 변경 사항
💬 추가 참고 사항
PR Type
Enhancement, Bug fix
Description
Add guest user support for "to sing" playlist with local storage persistence
Refactor popular page to use client-side rendering with real-time refresh UI
Implement conditional API response types and improve query management
Separate authenticated and guest data flows across multiple hooks and stores
Diagram Walkthrough
File Walkthrough
21 files
New API endpoint for guest to-sing songsUpdate to pass full song object for guest handlingAdd guest mode support with local state managementAdd guest API function for fetching to-sing songsAdd authentication check to enable/disable queryAdd authentication check to enable/disable querySet stale time to zero for real-time data refreshSupport guest data from local store and remove debouncingRefactor to store full song objects with order weightsApply conditional typing for API response data fieldPass authentication status to like song queryPass authentication status to save song queryRefactor to client component with refresh buttonRemove client-side logic and simplify component structureUpdate guest to-sing detection for full song objectsExtract button component from page for reusabilityUpdate hook import name for consistencyRefactor to use extracted AddSongButton componentPass authentication status to song queriesAdd new progress timer component for auto-refresh UIAdd React Query DevTools for development debugging1 files
Remove unnecessary query invalidations for performance1 files
Remove unused loading overlay component1 files
Clean up commented code in loading component1 files
Update React Query DevTools to version 5.91.21 files
Remove ESLint configuration file1 files
Add custom Git automation rules documentation1 files