Skip to content

Feat/guest feature#138

Merged
GulSam00 merged 12 commits intodevelopfrom
feat/guestFeature
Feb 7, 2026
Merged

Feat/guest feature#138
GulSam00 merged 12 commits intodevelopfrom
feat/guestFeature

Conversation

@GulSam00
Copy link
Owner

@GulSam00 GulSam00 commented Feb 7, 2026

User description

📌 PR 제목

Feat : 게스트 상태에서 부를 곡 추가 변경

📌 변경 사항

  • 인기곡 페이지 리펙토링
  • 인증값에 따라 비회원 상태에서 query 제어
  • 게스트 상태에서 부를곡 추가

💬 추가 참고 사항


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

flowchart LR
  A["Guest User"] -->|"Add Song"| B["useGuestToSingStore"]
  B -->|"Persist"| C["Local Storage"]
  C -->|"Retrieve"| D["useToSingSong Hook"]
  D -->|"Display"| E["SongList Component"]
  F["Authenticated User"] -->|"Add Song"| G["API Request"]
  G -->|"Server"| H["tosing/guest Route"]
  H -->|"Supabase"| I["Database"]
  I -->|"Return"| D
  J["Popular Page"] -->|"Query"| K["useSongThumbQuery"]
  K -->|"Fetch"| L["API"]
  L -->|"Display"| M["PopularRankingList"]
Loading

File Walkthrough

Relevant files
Enhancement
21 files
route.ts
New API endpoint for guest to-sing songs                                 
+52/-0   
useSearchSong.ts
Update to pass full song object for guest handling             
+6/-6     
useToSingSong.ts
Add guest mode support with local state management             
+36/-2   
tosing.ts
Add guest API function for fetching to-sing songs               
+7/-0     
likeSongQuery.ts
Add authentication check to enable/disable query                 
+2/-3     
saveSongQuery.ts
Add authentication check to enable/disable query                 
+2/-3     
songThumbQuery.ts
Set stale time to zero for real-time data refresh               
+3/-2     
tosingSongQuery.ts
Support guest data from local store and remove debouncing
+27/-50 
useGuestToSingStore.ts
Refactor to store full song objects with order weights     
+31/-26 
apiRoute.ts
Apply conditional typing for API response data field         
+13/-4   
page.tsx
Pass authentication status to like song query                       
+3/-1     
page.tsx
Pass authentication status to save song query                       
+6/-3     
PopularRankingList.tsx
Refactor to client component with refresh button                 
+20/-11 
page.tsx
Remove client-side logic and simplify component structure
+1/-12   
HomePage.tsx
Update guest to-sing detection for full song objects         
+3/-3     
AddSongButton.tsx
Extract button component from page for reusability             
+29/-0   
SongList.tsx
Update hook import name for consistency                                   
+2/-2     
page.tsx
Refactor to use extracted AddSongButton component               
+2/-20   
AddListModal.tsx
Pass authentication status to song queries                             
+6/-2     
IntervalProgress.tsx
Add new progress timer component for auto-refresh UI         
+126/-0 
query.tsx
Add React Query DevTools for development debugging             
+7/-1     
Bug fix
1 files
searchSongQuery.ts
Remove unnecessary query invalidations for performance     
+0/-3     
Miscellaneous
1 files
LoadingOverlay.tsx
Remove unused loading overlay component                                   
+0/-24   
Formatting
1 files
StaticLoading.tsx
Clean up commented code in loading component                         
+0/-1     
Dependencies
1 files
package.json
Update React Query DevTools to version 5.91.2                       
+1/-1     
Configuration changes
1 files
settings.json
Remove ESLint configuration file                                                 
+0/-7     
Documentation
1 files
GEMINI.md
Add custom Git automation rules documentation                       
+7/-0     
Additional files
1 files
pnpm-lock.yaml +1/-1     

@GulSam00
Copy link
Owner Author

GulSam00 commented Feb 7, 2026

@CodiumAI-Agent /improve

@vercel
Copy link
Contributor

vercel bot commented Feb 7, 2026

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

Project Deployment Actions Updated (UTC)
singcode Error Error Feb 7, 2026 8:57am

@GulSam00
Copy link
Owner Author

GulSam00 commented Feb 7, 2026

@CodiumAI-Agent /improve

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

Comment on lines +63 to +70
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;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

progress가 100 이상이 된 이후에도 interval이 계속 돌면서 onComplete가 매 tick마다 반복 호출될 수 있습니다(현재 state가 100에 고정되어도 next 계산이 계속 >=100). 완료 시점에 interval을 중단하거나, 이미 완료된 경우 onComplete를 한 번만 호출하도록 가드 로직을 추가해주세요.

Copilot uses AI. Check for mistakes.
Comment on lines 23 to 27
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

ReactQueryDevtools가 항상 렌더링되면 프로덕션 번들/런타임 오버헤드 및 디버그 UI 노출 가능성이 있습니다. 개발 환경에서만 렌더링되도록 조건부로 감싸거나(dynamic import 포함) 빌드 환경 분기 처리를 해주세요.

Copilot uses AI. Check for mistakes.
<CardTitle className="text-xl">추천 곡 순위</CardTitle>
{/* <IntervalProgress duration={5000} onComplete={() => refetch()} isLoading={isFetching} /> */}

<RotateCw onClick={() => refetch()} className="cursor-pointer hover:animate-spin" />
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

RotateCw 아이콘에 onClick만 달려 있으면 키보드 포커스/Enter 동작, 스크린리더 라벨 등이 제공되지 않아 접근성 이슈가 생깁니다. 버튼 요소로 감싸고 aria-label을 추가(또는 Button 컴포넌트 사용)해서 클릭/키보드 조작 모두 가능하게 해주세요.

Suggested change
<RotateCw onClick={() => refetch()} className="cursor-pointer hover:animate-spin" />
<button
type="button"
onClick={() => refetch()}
aria-label="추천 곡 순위 새로고침"
className="cursor-pointer hover:animate-spin"
>
<RotateCw />
</button>

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 52
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 };
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

swapGuestToSingSongs에서 moveIndex에 대한 범위 체크가 없어 음수/범위 초과 값이 들어오면 예상치 못한 위치로 이동할 수 있습니다(splice의 음수 인덱싱 포함). 0~length 범위로 clamp하거나 잘못된 값이면 state를 그대로 반환하도록 방어 로직을 추가해주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +16
const supabase = await createClient();

const { data, error } = await supabase
.from('songs')
.select('*', { count: 'exact' })
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

GET /songs/tosing/guest에서 songIds가 비어있는 경우에도 .in('id', ids)를 호출하고 있는데, Supabase에서는 빈 배열 in 필터가 에러가 날 수 있습니다. ids.length===0이면 즉시 success:true, data:[]를 반환하도록 처리하고, select의 count 옵션은 사용하지 않으니 제거해서 쿼리 비용도 줄이는 게 좋습니다.

Suggested change
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('*')

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
const toSingSongs = data.map((song, index) => ({
songs: song,
order_weight: index,
}));

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

현재 .in('id', ids)로 가져온 data의 순서는 보장되지 않아서, order_weight를 map index로 두면 클라이언트가 기대한(요청한 ids 순서) 정렬과 달라질 수 있습니다. ids 순서대로 결과를 재정렬한 뒤 order_weight를 부여하거나, 쿼리에서 정렬 기준을 명확히 지정해주세요.

Suggested change
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);

Copilot uses AI. Check for mistakes.
export function useToSingSongQuery(isAuthenticated: boolean, guestToSingSongs: ToSingSong[]) {
return useQuery({
queryKey: ['toSingSong'],
queryKey: isAuthenticated ? ['toSingSong'] : ['toSingSong', 'guest', guestToSingSongs],
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

게스트 상태일 때 queryKey에 guestToSingSongs(객체 배열)를 그대로 넣으면, 키 해시가 커지고(객체 전체 포함) 불필요한 캐시 미스/리렌더가 발생하기 쉽습니다. 게스트 키에는 songId 목록(예: ids 배열)이나 버전/길이처럼 안정적이고 작은 값만 포함하도록 바꾸는 편이 안전합니다.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +9
// export interface ApiSuccessResponse<T> {
// success: true;
// data?: T;
// hasNext?: boolean;
// // data: T; 타입 에러
// }

// 조건부 타입 적용
// T가 void(비어있음)면 data 필드 자체를 없애고(또는 optional never), T가 있으면 data를 필수(Required)로 구성.
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

ApiSuccessResponse 타입 변경 자체는 좋지만, 이전 interface 정의를 주석으로 남겨두면 파일이 불필요하게 길어지고 유지보수가 어려워집니다. 히스토리는 VCS에 있으니 주석 블록은 제거하고, 필요하면 조건부 타입 설명만 간단히 남겨주세요.

Suggested change
// export interface ApiSuccessResponse<T> {
// success: true;
// data?: T;
// hasNext?: boolean;
// // data: T; 타입 에러
// }
// 조건부 타입 적용
// T가 void(비어있음)면 data 필드 자체를 없애고(또는 optional never), T가 있으면 data를 필수(Required)로 구성.
// ApiSuccessResponse는 제네릭 T에 따라 data 필드 존재 여부를 결정하는 조건부 타입입니다.

Copilot uses AI. Check for mistakes.
import { useSongThumbQuery } from '@/queries/songThumbQuery';

export default function PopularRankingList() {
const { data, isPending, refetch, isFetching } = useSongThumbQuery();
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Unused variable isFetching.

Suggested change
const { data, isPending, refetch, isFetching } = useSongThumbQuery();
const { data, isPending, refetch } = useSongThumbQuery();

Copilot uses AI. Check for mistakes.
@GulSam00
Copy link
Owner Author

GulSam00 commented Feb 7, 2026

/describe

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 7, 2026

PR Description updated to latest commit (0cc2adc)

Choose a reason for hiding this comment

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

File Walkthrough

route.ts
New API endpoint for guest to-sing songs                                 
useSearchSong.ts
Update to pass full song object for guest handling             
useToSingSong.ts
Add guest mode support with local state management             
tosing.ts
Add guest API function for fetching to-sing songs               
likeSongQuery.ts
Add authentication check to enable/disable query                 
saveSongQuery.ts
Add authentication check to enable/disable query                 
songThumbQuery.ts
Set stale time to zero for real-time data refresh               
tosingSongQuery.ts
Support guest data from local store and remove debouncing
useGuestToSingStore.ts
Refactor to store full song objects with order weights     
apiRoute.ts
Apply conditional typing for API response data field         
page.tsx
Pass authentication status to like song query                       
page.tsx
Pass authentication status to save song query                       
PopularRankingList.tsx
Refactor to client component with refresh button                 
page.tsx
Remove client-side logic and simplify component structure
HomePage.tsx
Update guest to-sing detection for full song objects         
AddSongButton.tsx
Extract button component from page for reusability             
SongList.tsx
Update hook import name for consistency                                   
page.tsx
Refactor to use extracted AddSongButton component               
AddListModal.tsx
Pass authentication status to song queries                             
IntervalProgress.tsx
Add new progress timer component for auto-refresh UI         
query.tsx
Add React Query DevTools for development debugging             
searchSongQuery.ts
Remove unnecessary query invalidations for performance     
LoadingOverlay.tsx
Remove unused loading overlay component                                   
StaticLoading.tsx
Clean up commented code in loading component                         
package.json
Update React Query DevTools to version 5.91.2                       
settings.json
Remove ESLint configuration file                                                 
GEMINI.md
Add custom Git automation rules documentation                       
pnpm-lock.yaml

@GulSam00
Copy link
Owner Author

GulSam00 commented Feb 7, 2026

/review

@qodo-code-review
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

Optimistic update에서 queryClient.setQueryData 콜백이 필터링 결과를 반환하지 않아 캐시가 undefined로 설정될 수 있습니다. 삭제 시 UI가 갱신되지 않거나 예기치 않은 상태가 될 수 있으니 콜백에서 새 배열을 명시적으로 반환하도록 확인이 필요합니다.

return useMutation({
  mutationFn: (songId: string) => deleteToSingSong({ songId }),
  onMutate: async (songId: string) => {
    queryClient.cancelQueries({ queryKey: ['toSingSong'] });
    const prev = queryClient.getQueryData(['toSingSong']);
    queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => {
      old.filter(song => song.songs.id !== songId);
    });
    return { prev };
API Mismatch

게스트 API 호출 시 쿼리 파라미터 키가 서버 라우트에서 기대하는 형태(songIds[])와 다를 수 있습니다. 현재 클라이언트는 songIds로 보내고 서버는 songIds[]getAll로 읽고 있어, ids가 비어 조회 결과가 항상 빈 값이 되는지 확인이 필요합니다.

export async function getToSingSongGuest(songIds: string[]) {
  const response = await instance.get<ApiResponse<ToSingSong[]>>('/songs/tosing/guest', {
    params: { songIds },
  });
  return response.data;
}
Perf Concern

staleTime: 0, gcTime: 0 설정으로 마운트/리렌더/포커스 변화 등에 따라 캐시가 즉시 폐기되고 매번 재요청될 수 있습니다. 실시간 새로고침 의도라면 refetch 트리거를 제한하거나 최소한의 캐시 유지/쓰로틀링 전략이 필요한지 점검이 필요합니다.

export const useSongThumbQuery = () => {
  return useQuery({
    queryKey: ['songThumb'],
    queryFn: async () => {
      const response = await getSongThumbList();

      if (!response.success) {
        return [];
      }
      return response.data;
    },
    staleTime: 0, // 데이터를 받자마자 "상한 것(Stale)"으로 취급 -> 다시 조회할 명분 생성
    gcTime: 0, // (구 cacheTime) 언마운트 되는 즉시 메모리에서 삭제 -> 캐시가 없으니 무조건 새로 요청  });
  });

@GulSam00 GulSam00 merged commit 6fd071d into develop Feb 7, 2026
@GulSam00 GulSam00 deleted the feat/guestFeature branch February 7, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant