- {songStats.length > 0 ? (
- songStats.map((item, index) => (
+ {data && data.length > 0 ? (
+ data.map((item, index) => (
))
) : (
diff --git a/apps/web/src/app/popular/page.tsx b/apps/web/src/app/popular/page.tsx
index 4fc6ae1..b97552d 100644
--- a/apps/web/src/app/popular/page.tsx
+++ b/apps/web/src/app/popular/page.tsx
@@ -1,26 +1,15 @@
-'use client';
-
-import { useState } from 'react';
-
-import StaticLoading from '@/components/StaticLoading';
import { ScrollArea } from '@/components/ui/scroll-area';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { useSongThumbQuery } from '@/queries/songThumbQuery';
import PopularRankingList from './PopularRankingList';
export default function PopularPage() {
- const { isPending, data } = useSongThumbQuery();
-
- if (isPending || !data) return
;
-
return (
인기 노래
{/* 추천 곡 순위 */}
-
+
);
diff --git a/apps/web/src/app/search/HomePage.tsx b/apps/web/src/app/search/HomePage.tsx
index f6e40e1..76553a7 100644
--- a/apps/web/src/app/search/HomePage.tsx
+++ b/apps/web/src/app/search/HomePage.tsx
@@ -54,11 +54,11 @@ export default function SearchPage() {
const { ref, inView } = useInView();
const { searchHistory, removeFromHistory } = useSearchHistoryStore();
- const { localToSingSongIds } = useGuestToSingStore();
+ const { guestToSingSongs } = useGuestToSingStore();
const isToSing = (song: SearchSong, songId: string) => {
if (!isAuthenticated) {
- return localToSingSongIds?.includes(songId);
+ return guestToSingSongs?.some(item => item.songs.id === songId);
}
return song.isToSing;
};
@@ -184,7 +184,7 @@ export default function SearchPage() {
isLike={song.isLike}
isSave={song.isSave}
onToggleToSing={() =>
- handleToggleToSing(song.id, isToSing(song, song.id) ? 'DELETE' : 'POST')
+ handleToggleToSing(song, isToSing(song, song.id) ? 'DELETE' : 'POST')
}
onToggleLike={() => handleToggleLike(song.id, song.isLike ? 'DELETE' : 'POST')}
onClickSave={() => handleToggleSave(song, song.isSave ? 'PATCH' : 'POST')}
diff --git a/apps/web/src/app/tosing/AddListModal.tsx b/apps/web/src/app/tosing/AddListModal.tsx
index 98b826b..2f46caa 100644
--- a/apps/web/src/app/tosing/AddListModal.tsx
+++ b/apps/web/src/app/tosing/AddListModal.tsx
@@ -14,6 +14,7 @@ import useAddSongList, { type TabType } from '@/hooks/useAddSongList';
import { useLikeSongQuery } from '@/queries/likeSongQuery';
// import { useSaveSongFolderQuery } from '@/queries/saveSongFolderQuery';
import { useSaveSongQuery } from '@/queries/saveSongQuery';
+import useAuthStore from '@/stores/useAuthStore';
import ModalSongItem from './ModalSongItem';
@@ -32,9 +33,12 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) {
totalSelectedCount,
} = useAddSongList();
- const { data: likedSongs, isLoading: isLoadingLikedSongs } = useLikeSongQuery();
+ const { isAuthenticated } = useAuthStore();
- const { data: saveSongFolders, isLoading: isLoadingSongFolders } = useSaveSongQuery();
+ const { data: likedSongs, isLoading: isLoadingLikedSongs } = useLikeSongQuery(isAuthenticated);
+
+ const { data: saveSongFolders, isLoading: isLoadingSongFolders } =
+ useSaveSongQuery(isAuthenticated);
const isLoading = isLoadingLikedSongs || isLoadingSongFolders;
diff --git a/apps/web/src/app/tosing/AddSongButton.tsx b/apps/web/src/app/tosing/AddSongButton.tsx
new file mode 100644
index 0000000..7a0d53b
--- /dev/null
+++ b/apps/web/src/app/tosing/AddSongButton.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import { AirplayIcon } from 'lucide-react';
+import { useState } from 'react';
+
+import { Button } from '@/components/ui/button';
+
+import AddListModal from './AddListModal';
+
+export default function AddSongButton() {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ return (
+ <>
+
+
+ {/* 모달도 여기서 렌더링 */}
+
setIsModalOpen(false)} />
+ >
+ );
+}
diff --git a/apps/web/src/app/tosing/SongList.tsx b/apps/web/src/app/tosing/SongList.tsx
index 08bb506..810c4c6 100644
--- a/apps/web/src/app/tosing/SongList.tsx
+++ b/apps/web/src/app/tosing/SongList.tsx
@@ -17,7 +17,7 @@ import {
} from '@dnd-kit/sortable';
import StaticLoading from '@/components/StaticLoading';
-import useSong from '@/hooks/useSong';
+import useToSingSong from '@/hooks/useToSingSong';
import { ToSingSong } from '@/types/song';
import SongCard from './SongCard';
@@ -30,7 +30,7 @@ export default function SongList() {
handleDelete,
handleMoveToTop,
handleMoveToBottom,
- } = useSong();
+ } = useToSingSong();
const sensors = useSensors(
useSensor(PointerSensor),
diff --git a/apps/web/src/app/tosing/page.tsx b/apps/web/src/app/tosing/page.tsx
index 5b71e51..3125f7a 100644
--- a/apps/web/src/app/tosing/page.tsx
+++ b/apps/web/src/app/tosing/page.tsx
@@ -1,36 +1,18 @@
-'use client';
-
-import { AirplayIcon } from 'lucide-react';
-import { useState } from 'react';
-
-import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
-import AddListModal from './AddListModal';
+import AddSongButton from './AddSongButton';
import SongList from './SongList';
export default function HomePage() {
- const [isModalOpen, setIsModalOpen] = useState(false);
-
return (
노래방 플레이리스트
-
+
-
-
setIsModalOpen(false)} />
);
}
diff --git a/apps/web/src/components/LoadingOverlay.tsx b/apps/web/src/components/LoadingOverlay.tsx
deleted file mode 100644
index 42a011d..0000000
--- a/apps/web/src/components/LoadingOverlay.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-// import useLoadingStore from '@/stores/useLoadingStore';
-
-export default function LoadingOverlay() {
- // const { isLoading } = useLoadingStore();
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- if (!isMounted) return null;
-
- // if (!isLoading) return null;
-
- return (
-
- );
-}
diff --git a/apps/web/src/components/StaticLoading.tsx b/apps/web/src/components/StaticLoading.tsx
index 6c587cd..33c0544 100644
--- a/apps/web/src/components/StaticLoading.tsx
+++ b/apps/web/src/components/StaticLoading.tsx
@@ -3,7 +3,6 @@ import { Loader2 } from 'lucide-react';
export default function StaticLoading() {
return (
);
diff --git a/apps/web/src/components/ui/IntervalProgress.tsx b/apps/web/src/components/ui/IntervalProgress.tsx
new file mode 100644
index 0000000..2b05def
--- /dev/null
+++ b/apps/web/src/components/ui/IntervalProgress.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import { Loader2 } from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+import { cn } from '@/utils/cn';
+
+interface IntervalProgressProps {
+ /**
+ * The duration in milliseconds for the progress to complete.
+ * @default 5000
+ */
+ duration?: number;
+ /**
+ * Callback function to be called when the progress completes.
+ */
+ onComplete?: () => void;
+ /**
+ * The size of the progress circle in pixels.
+ * @default 24
+ */
+ size?: number;
+ /**
+ * The stroke width of the progress circle.
+ * @default 3
+ */
+ strokeWidth?: number;
+ /**
+ * Class name for custom styling.
+ */
+ className?: string;
+ /**
+ * Whether the timer is currently active.
+ * @default true
+ */
+ isActive?: boolean;
+ /**
+ * Whether the component is in a loading state.
+ * During loading, the timer pauses and a spinner is shown.
+ * When loading finishes, the timer resets and restarts.
+ */
+ isLoading?: boolean;
+}
+
+export default function IntervalProgress({
+ duration = 5000,
+ onComplete,
+ size = 24,
+ strokeWidth = 3,
+ className,
+ isActive = true,
+ isLoading = false,
+}: IntervalProgressProps) {
+ const [progress, setProgress] = useState(0);
+ const updateInterval = 50; // Update every 50ms for smooth animation
+
+ useEffect(() => {
+ // If loading, don't run the timer
+ // Also, if we just finished loading (isLoading became false), we might want to reset?
+ // Actually, let's handle reset in a separate effect or here.
+ if (!isActive || isLoading) return;
+
+ 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;
+ });
+ }, updateInterval);
+
+ return () => clearInterval(interval);
+ }, [isActive, duration, onComplete, isLoading]);
+
+ // Reset progress when loading finishes
+ useEffect(() => {
+ if (!isLoading) {
+ setProgress(0);
+ }
+ }, [isLoading]);
+
+ const radius = (size - strokeWidth) / 2;
+ const circumference = radius * 2 * Math.PI;
+ const dashoffset = circumference - (progress / 100) * circumference;
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/hooks/useSearchSong.ts b/apps/web/src/hooks/useSearchSong.ts
index 7cc9809..3682606 100644
--- a/apps/web/src/hooks/useSearchSong.ts
+++ b/apps/web/src/hooks/useSearchSong.ts
@@ -12,7 +12,7 @@ import useAuthStore from '@/stores/useAuthStore';
import useGuestToSingStore from '@/stores/useGuestToSingStore';
import useSearchHistoryStore from '@/stores/useSearchHistoryStore';
import { Method } from '@/types/common';
-import { SearchSong } from '@/types/song';
+import { SearchSong, Song } from '@/types/song';
type SearchType = 'all' | 'title' | 'artist';
@@ -47,7 +47,7 @@ export default function useSearchSong() {
} = useInfiniteSearchSongQuery(query, searchType, isAuthenticated);
const { addToHistory } = useSearchHistoryStore();
- const { addSong, removeSong } = useGuestToSingStore();
+ const { addGuestToSingSong, removeGuestToSingSong } = useGuestToSingStore();
const handleSearch = () => {
// trim 제거
@@ -71,12 +71,12 @@ export default function useSearchSong() {
setSearchType(value as SearchType);
};
- const handleToggleToSing = async (songId: string, method: Method) => {
+ const handleToggleToSing = async (song: Song, method: Method) => {
if (!isAuthenticated) {
if (method === 'POST') {
- addSong(songId);
+ addGuestToSingSong(song);
} else {
- removeSong(songId);
+ removeGuestToSingSong(song.id);
}
return;
}
@@ -85,7 +85,7 @@ export default function useSearchSong() {
toast.error('요청 중입니다. 잠시 후 다시 시도해주세요.');
return;
}
- toggleToSing({ songId, method });
+ toggleToSing({ songId: song.id, method });
};
const handleToggleLike = async (songId: string, method: Method) => {
diff --git a/apps/web/src/hooks/useSong.ts b/apps/web/src/hooks/useToSingSong.ts
similarity index 64%
rename from apps/web/src/hooks/useSong.ts
rename to apps/web/src/hooks/useToSingSong.ts
index fcc3f32..3ee1d11 100644
--- a/apps/web/src/hooks/useSong.ts
+++ b/apps/web/src/hooks/useToSingSong.ts
@@ -7,18 +7,34 @@ import {
usePatchToSingSongMutation,
useToSingSongQuery,
} from '@/queries/tosingSongQuery';
+import useAuthStore from '@/stores/useAuthStore';
+import useGuestToSingStore from '@/stores/useGuestToSingStore';
-export default function useSong() {
- const { data, isLoading } = useToSingSongQuery();
+export default function useToSingSong() {
+ const { isAuthenticated } = useAuthStore();
+ const { guestToSingSongs, swapGuestToSingSongs, removeGuestToSingSong } = useGuestToSingStore();
+
+ const { data, isLoading } = useToSingSongQuery(isAuthenticated, guestToSingSongs);
const { mutate: patchToSingSong } = usePatchToSingSongMutation();
const { mutate: deleteToSingSong } = useDeleteToSingSongMutation();
const toSingSongs = data ?? [];
const handleDragEnd = (event: DragEndEvent) => {
+ // 일단 guest일 때는 return 조치, 후에 local단에서 순서 조정 가능
const { active, over } = event;
if (!over || active.id === over.id) return;
+ if (!isAuthenticated) {
+ const oldIndex = toSingSongs.findIndex(item => item.songs.id === active.id);
+ const newIndex = toSingSongs.findIndex(item => item.songs.id === over.id);
+
+ if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
+ swapGuestToSingSongs(active.id as string, newIndex);
+ }
+ return;
+ }
+
const oldIndex = toSingSongs.findIndex(item => item.songs.id === active.id);
const newIndex = toSingSongs.findIndex(item => item.songs.id === over.id);
@@ -49,12 +65,23 @@ export default function useSong() {
};
const handleDelete = (songId: string) => {
+ if (!isAuthenticated) {
+ removeGuestToSingSong(songId);
+ return;
+ }
deleteToSingSong(songId);
};
const handleMoveToTop = (songId: string, oldIndex: number) => {
+ // 일단 guest일 때는 return 조치, 후에 local단에서 순서 조정 가능
+
if (oldIndex === 0) return;
+ if (!isAuthenticated) {
+ swapGuestToSingSongs(songId, 0);
+ return;
+ }
+
const newItems = arrayMove(toSingSongs, oldIndex, 0);
const newWeight = toSingSongs[0].order_weight - 1;
@@ -66,9 +93,16 @@ export default function useSong() {
};
const handleMoveToBottom = (songId: string, oldIndex: number) => {
+ // 일단 guest일 때는 return 조치, 후에 local단에서 순서 조정 가능
+
const lastIndex = toSingSongs.length - 1;
if (oldIndex === lastIndex) return;
+ if (!isAuthenticated) {
+ swapGuestToSingSongs(songId, lastIndex);
+ return;
+ }
+
const newItems = arrayMove(toSingSongs, oldIndex, lastIndex);
const newWeight = toSingSongs[lastIndex].order_weight + 1;
diff --git a/apps/web/src/lib/api/tosing.ts b/apps/web/src/lib/api/tosing.ts
index cb20957..ed25688 100644
--- a/apps/web/src/lib/api/tosing.ts
+++ b/apps/web/src/lib/api/tosing.ts
@@ -8,6 +8,13 @@ export async function getToSingSong() {
return response.data;
}
+export async function getToSingSongGuest(songIds: string[]) {
+ const response = await instance.get>('/songs/tosing/guest', {
+ params: { songIds },
+ });
+ return response.data;
+}
+
export async function patchToSingSong(body: { songId: string; newWeight: number }) {
const response = await instance.patch>('/songs/tosing', body);
return response.data;
diff --git a/apps/web/src/queries/likeSongQuery.ts b/apps/web/src/queries/likeSongQuery.ts
index 328169a..8f97238 100644
--- a/apps/web/src/queries/likeSongQuery.ts
+++ b/apps/web/src/queries/likeSongQuery.ts
@@ -4,7 +4,7 @@ import { deleteLikeSongArray, getLikeSong } from '@/lib/api/likeSong';
import { PersonalSong } from '@/types/song';
// 🎵 좋아요 한 곡 리스트 가져오기
-export function useLikeSongQuery() {
+export function useLikeSongQuery(isAuthenticated: boolean) {
return useQuery({
queryKey: ['likeSong'],
queryFn: async () => {
@@ -14,8 +14,7 @@ export function useLikeSongQuery() {
}
return response.data || [];
},
- staleTime: 1000 * 60 * 5,
- gcTime: 1000 * 60 * 5,
+ enabled: isAuthenticated,
});
}
diff --git a/apps/web/src/queries/saveSongQuery.ts b/apps/web/src/queries/saveSongQuery.ts
index a0b9870..ac3e4ef 100644
--- a/apps/web/src/queries/saveSongQuery.ts
+++ b/apps/web/src/queries/saveSongQuery.ts
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { deleteSaveSong, getSaveSong, patchSaveSong } from '@/lib/api/saveSong';
import { SaveSong, SaveSongFolder } from '@/types/song';
-export function useSaveSongQuery() {
+export function useSaveSongQuery(isAuthenticated: boolean) {
return useQuery({
queryKey: ['saveSongFolder'],
queryFn: async () => {
@@ -30,8 +30,7 @@ export function useSaveSongQuery() {
return songFolders;
},
- staleTime: 1000 * 60 * 5,
- gcTime: 1000 * 60 * 5,
+ enabled: isAuthenticated,
});
}
diff --git a/apps/web/src/queries/searchSongQuery.ts b/apps/web/src/queries/searchSongQuery.ts
index ff47fa1..2eed3c1 100644
--- a/apps/web/src/queries/searchSongQuery.ts
+++ b/apps/web/src/queries/searchSongQuery.ts
@@ -133,7 +133,6 @@ export const useToggleToSingMutation = (query: string, searchType: string) => {
queryKey: ['searchSong', query, searchType],
});
queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
- queryClient.invalidateQueries({ queryKey: ['recentSingLog'] });
}, 1000);
},
});
@@ -185,7 +184,6 @@ export const useToggleLikeMutation = (query: string, searchType: string) => {
queryKey: ['searchSong', query, searchType],
});
queryClient.invalidateQueries({ queryKey: ['likeSong'] });
- queryClient.invalidateQueries({ queryKey: ['recentSingLog'] });
}, 1000);
},
});
@@ -230,7 +228,6 @@ export const useSaveMutation = () => {
});
queryClient.invalidateQueries({ queryKey: ['saveSongFolder'] });
queryClient.invalidateQueries({ queryKey: ['saveSongFolderList'] });
- queryClient.invalidateQueries({ queryKey: ['recentSingLog'] });
},
});
};
diff --git a/apps/web/src/queries/songThumbQuery.ts b/apps/web/src/queries/songThumbQuery.ts
index 34999af..68fb4c9 100644
--- a/apps/web/src/queries/songThumbQuery.ts
+++ b/apps/web/src/queries/songThumbQuery.ts
@@ -9,11 +9,12 @@ export const useSongThumbQuery = () => {
const response = await getSongThumbList();
if (!response.success) {
- return null;
+ return [];
}
return response.data;
},
- staleTime: 1000 * 60,
+ staleTime: 0, // 데이터를 받자마자 "상한 것(Stale)"으로 취급 -> 다시 조회할 명분 생성
+ gcTime: 0, // (구 cacheTime) 언마운트 되는 즉시 메모리에서 삭제 -> 캐시가 없으니 무조건 새로 요청 });
});
};
diff --git a/apps/web/src/queries/tosingSongQuery.ts b/apps/web/src/queries/tosingSongQuery.ts
index fe6c70f..18b3601 100644
--- a/apps/web/src/queries/tosingSongQuery.ts
+++ b/apps/web/src/queries/tosingSongQuery.ts
@@ -8,26 +8,30 @@ import {
} from '@/lib/api/tosing';
import { ToSingSong } from '@/types/song';
-let invalidateTimeout: NodeJS.Timeout | null = null;
+// let invalidateTimeout: NodeJS.Timeout | null = null;
-// 🎵 부를 노래 목록 가져오기
-export function useToSingSongQuery() {
+// 부를 노래 목록 가져오기
+export function useToSingSongQuery(isAuthenticated: boolean, guestToSingSongs: ToSingSong[]) {
return useQuery({
- queryKey: ['toSingSong'],
+ queryKey: isAuthenticated
+ ? ['toSingSong']
+ : ['toSingSong', 'guest', guestToSingSongs.map(song => song.songs.id)],
queryFn: async () => {
- const response = await getToSingSong();
- if (!response.success) {
- return [];
+ if (isAuthenticated) {
+ const response = await getToSingSong();
+ if (!response.success) {
+ return [];
+ }
+ return response.data || [];
+ } else {
+ // 게스트의 경우 로컬 스토리지 데이터 반환 (서버 요청 X)
+ return guestToSingSongs;
}
- return response.data || [];
},
- // DB의 값은 고정된 값이므로 캐시를 유지한다
- staleTime: 1000 * 60 * 5,
- gcTime: 1000 * 60 * 5,
});
}
-// 🎵 부를 노래 추가
+// 부를 노래 추가
export function usePostToSingSongMutation() {
const queryClient = useQueryClient();
@@ -35,9 +39,6 @@ export function usePostToSingSongMutation() {
mutationFn: (songIds: string[]) => postToSingSongArray({ songIds }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
- queryClient.invalidateQueries({ queryKey: ['likeSong'] });
- queryClient.invalidateQueries({ queryKey: ['saveSongFolder'] });
- queryClient.invalidateQueries({ queryKey: ['recentSingLog'] });
queryClient.invalidateQueries({ queryKey: ['searchSong'] });
},
onError: error => {
@@ -47,27 +48,7 @@ export function usePostToSingSongMutation() {
});
}
-// 🎵 여러 곡 부를 노래 추가
-export function usePostToSingSongArrayMutation() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (songIds: string[]) => postToSingSongArray({ songIds }),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
- queryClient.invalidateQueries({ queryKey: ['likeSong'] });
- queryClient.invalidateQueries({ queryKey: ['saveSongFolder'] });
- queryClient.invalidateQueries({ queryKey: ['recentSingLog'] });
- queryClient.invalidateQueries({ queryKey: ['searchSong'] });
- },
- onError: error => {
- console.error('error', error);
- alert(error.message ?? 'POST 실패');
- },
- });
-}
-
-// 🎵 부를 노래 삭제
+// 부를 노래 삭제
export function useDeleteToSingSongMutation() {
const queryClient = useQueryClient();
@@ -76,9 +57,9 @@ export function useDeleteToSingSongMutation() {
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),
- );
+ queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => {
+ old.filter(song => song.songs.id !== songId);
+ });
return { prev };
},
onError: (error, variables, context) => {
@@ -88,21 +69,19 @@ export function useDeleteToSingSongMutation() {
},
onSettled: () => {
// 1초 이내에 함수가 여러 번 호출되면, 1초 뒤 트리거를 계속해서 갱신
- if (invalidateTimeout) {
- clearTimeout(invalidateTimeout);
- }
- invalidateTimeout = setTimeout(() => {
- queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
- queryClient.invalidateQueries({ queryKey: ['likeSong'] });
- queryClient.invalidateQueries({ queryKey: ['saveSongFolder'] });
- queryClient.invalidateQueries({ queryKey: ['recentSingLog'] });
- queryClient.invalidateQueries({ queryKey: ['searchSong'] });
- }, 1000);
+ // if (invalidateTimeout) {
+ // clearTimeout(invalidateTimeout);
+ // }
+ // invalidateTimeout = setTimeout(() => {
+ // queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
+ // }, 1000);
+ queryClient.invalidateQueries({ queryKey: ['searchSong'] });
+ queryClient.invalidateQueries({ queryKey: ['toSingSong'] });
},
});
}
-// 🎵 부를 노래 순서 변경
+// 부를 노래 순서 변경
export function usePatchToSingSongMutation() {
const queryClient = useQueryClient();
diff --git a/apps/web/src/query.tsx b/apps/web/src/query.tsx
index c6d8b5b..e77c2ae 100644
--- a/apps/web/src/query.tsx
+++ b/apps/web/src/query.tsx
@@ -1,6 +1,7 @@
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export default function QueryProvider({ children }: { children: React.ReactNode }) {
@@ -19,5 +20,10 @@ export default function QueryProvider({ children }: { children: React.ReactNode
}),
);
- return {children};
+ return (
+
+ {children}
+ {process.env.NODE_ENV === 'development' && }
+
+ );
}
diff --git a/apps/web/src/stores/useGuestToSingStore.ts b/apps/web/src/stores/useGuestToSingStore.ts
index d7e90e2..ae6d615 100644
--- a/apps/web/src/stores/useGuestToSingStore.ts
+++ b/apps/web/src/stores/useGuestToSingStore.ts
@@ -1,53 +1,59 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
+import { Song, ToSingSong } from '@/types/song';
+
interface GuestToSingState {
- localToSingSongIds: string[];
- addSong: (songId: string) => void;
- removeSong: (songId: string) => void;
- swapSongs: (fromIndex: number, toIndex: number) => void;
- clearSongs: () => void;
+ guestToSingSongs: ToSingSong[];
+ addGuestToSingSong: (song: Song) => void;
+ removeGuestToSingSong: (songId: string) => void;
+ swapGuestToSingSongs: (targetId: string, moveIndex: number) => void;
+ clearGuestToSingSongs: () => void;
}
const GUEST_TO_SING_KEY = 'guest_to_sing';
const initialState = {
- localToSingSongIds: [] as string[],
+ guestToSingSongs: [] as ToSingSong[],
};
const useGuestToSingStore = create(
persist(
set => ({
...initialState,
- addSong: (songId: string) => {
+ addGuestToSingSong: (song: Song) => {
set(state => {
- // 중복 방지 (필요 시 정책 변경 가능)
- if (state.localToSingSongIds.includes(songId)) return state;
- return { localToSingSongIds: [...state.localToSingSongIds, songId] };
+ // 중복 방지
+ if (state.guestToSingSongs.some(item => item.songs.id === song.id)) return state;
+
+ const newToSingSong: ToSingSong = {
+ order_weight: 0, // 로컬에서는 index가 순서이므로 weight는 의미 없음 (0으로 고정)
+ songs: song, // song 객체 전체 저장
+ };
+
+ return { guestToSingSongs: [...state.guestToSingSongs, newToSingSong] };
});
},
- removeSong: (songId: string) => {
+ removeGuestToSingSong: (songId: string) => {
set(state => ({
- localToSingSongIds: state.localToSingSongIds.filter(id => id !== songId),
+ guestToSingSongs: state.guestToSingSongs.filter(item => item.songs.id !== songId),
}));
},
- swapSongs: (fromIndex: number, toIndex: number) => {
+ 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 };
+ if (moveIndex < 0 || moveIndex >= state.guestToSingSongs.length) return state;
+ 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 };
});
},
- clearSongs: () => {
+ clearGuestToSingSongs: () => {
set(initialState);
},
}),
diff --git a/apps/web/src/types/apiRoute.ts b/apps/web/src/types/apiRoute.ts
index 239cc83..1de8512 100644
--- a/apps/web/src/types/apiRoute.ts
+++ b/apps/web/src/types/apiRoute.ts
@@ -1,9 +1,18 @@
-export interface ApiSuccessResponse {
+// export interface ApiSuccessResponse {
+// success: true;
+// data?: T;
+// hasNext?: boolean;
+// // data: T; 타입 에러
+// }
+
+// 조건부 타입 적용
+// T가 void(비어있음)면 data 필드 자체를 없애고(또는 optional never), T가 있으면 data를 필수(Required)로 구성.
+export type ApiSuccessResponse = {
success: true;
- data?: T;
hasNext?: boolean;
- // data: T; 타입 에러
-}
+} & (T extends void
+ ? { data?: never } // T가 void면 data는 없어야 함
+ : { data: T }); // T가 있으면 data는 필수
export interface ApiErrorResponse {
success: false;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e7549fd..fe2df0c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -200,7 +200,7 @@ importers:
specifier: ^5.68.0
version: 5.90.16(react@19.2.3)
'@tanstack/react-query-devtools':
- specifier: ^5.68.0
+ specifier: ^5.91.2
version: 5.91.2(@tanstack/react-query@5.90.16(react@19.2.3))(react@19.2.3)
'@vercel/analytics':
specifier: ^1.5.0