diff --git a/apps/web/src/components/ui/IntervalProgress.tsx b/apps/web/src/components/ui/IntervalProgress.tsx
new file mode 100644
index 00000000..2b05defc
--- /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 d429a116..f33011c8 100644
--- a/apps/web/src/hooks/useSearchSong.ts
+++ b/apps/web/src/hooks/useSearchSong.ts
@@ -9,9 +9,11 @@ import {
useToggleToSingMutation,
} from '@/queries/searchSongQuery';
import useAuthStore from '@/stores/useAuthStore';
+import useFooterAnimateStore from '@/stores/useFooterAnimateStore';
+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';
@@ -45,7 +47,9 @@ export default function useSearchSong() {
isError,
} = useInfiniteSearchSongQuery(query, searchType, isAuthenticated);
+ const { setFooterAnimateKey } = useFooterAnimateStore();
const { addToHistory } = useSearchHistoryStore();
+ const { addGuestToSingSong, removeGuestToSingSong } = useGuestToSingStore();
const handleSearch = () => {
// trim ์ ๊ฑฐ
@@ -69,9 +73,14 @@ export default function useSearchSong() {
setSearchType(value as SearchType);
};
- const handleToggleToSing = async (songId: string, method: Method) => {
+ const handleToggleToSing = async (song: Song, method: Method) => {
if (!isAuthenticated) {
- toast.error('๋ก๊ทธ์ธ์ด ํ์ํด์.');
+ if (method === 'POST') {
+ addGuestToSingSong(song);
+ setFooterAnimateKey('TOSING');
+ } else {
+ removeGuestToSingSong(song.id);
+ }
return;
}
@@ -79,7 +88,11 @@ export default function useSearchSong() {
toast.error('์์ฒญ ์ค์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.');
return;
}
- toggleToSing({ songId, method });
+
+ if (method === 'POST') {
+ setFooterAnimateKey('TOSING');
+ }
+ toggleToSing({ songId: song.id, method });
};
const handleToggleLike = async (songId: string, method: Method) => {
@@ -92,6 +105,10 @@ export default function useSearchSong() {
toast.error('์์ฒญ ์ค์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.');
return;
}
+
+ if (method === 'POST') {
+ setFooterAnimateKey('INFO');
+ }
toggleLike({ songId, method });
};
@@ -110,6 +127,8 @@ export default function useSearchSong() {
toast.error('์์ฒญ ์ค์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.');
return;
}
+
+ setFooterAnimateKey('INFO');
postSong({ songId, folderName, query, searchType });
};
@@ -119,6 +138,7 @@ export default function useSearchSong() {
return;
}
+ setFooterAnimateKey('INFO');
moveSong({ songIdArray: [songId], folderId });
};
@@ -145,5 +165,7 @@ export default function useSearchSong() {
selectedSaveSong,
postSaveSong,
patchSaveSong,
+
+ isAuthenticated,
};
}
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 fcc3f32c..3ee1d11d 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 cb20957d..ed25688a 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 328169a6..8f972386 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 a0b98704..ac3e4eff 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 ff47fa19..2eed3c18 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 34999af8..68fb4c91 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 fe6c70f3..18b3601b 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 c6d8b5b5..e77c2aec 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/useFooterAnimateStore.ts b/apps/web/src/stores/useFooterAnimateStore.ts
new file mode 100644
index 00000000..060208d9
--- /dev/null
+++ b/apps/web/src/stores/useFooterAnimateStore.ts
@@ -0,0 +1,36 @@
+import { create } from 'zustand';
+
+export type FooterKey = 'SEARCH' | 'RECENT' | 'TOSING' | 'POPULAR' | 'INFO' | null;
+
+interface FooterStore {
+ footerAnimateKey: FooterKey;
+ timeoutId: ReturnType | null;
+ setFooterAnimateKey: (key: FooterKey) => void;
+}
+
+const initialState = {
+ footerAnimateKey: null,
+ timeoutId: null,
+};
+
+const useFooterAnimateStore = create((set, get) => ({
+ ...initialState,
+
+ setFooterAnimateKey: key => {
+ const { timeoutId } = get();
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ set({ footerAnimateKey: key });
+
+ const newTimeoutId = setTimeout(() => {
+ set({ footerAnimateKey: null, timeoutId: null });
+ }, 300);
+
+ set({ timeoutId: newTimeoutId });
+ },
+}));
+
+export default useFooterAnimateStore;
diff --git a/apps/web/src/stores/useGuestToSingStore.ts b/apps/web/src/stores/useGuestToSingStore.ts
new file mode 100644
index 00000000..ae6d615a
--- /dev/null
+++ b/apps/web/src/stores/useGuestToSingStore.ts
@@ -0,0 +1,67 @@
+import { create } from 'zustand';
+import { createJSONStorage, persist } from 'zustand/middleware';
+
+import { Song, ToSingSong } from '@/types/song';
+
+interface GuestToSingState {
+ 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 = {
+ guestToSingSongs: [] as ToSingSong[],
+};
+
+const useGuestToSingStore = create(
+ persist(
+ set => ({
+ ...initialState,
+ addGuestToSingSong: (song: Song) => {
+ set(state => {
+ // ์ค๋ณต ๋ฐฉ์ง
+ 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] };
+ });
+ },
+ removeGuestToSingSong: (songId: string) => {
+ set(state => ({
+ guestToSingSongs: state.guestToSingSongs.filter(item => item.songs.id !== songId),
+ }));
+ },
+ swapGuestToSingSongs: (targetId: string, moveIndex: number) => {
+ set(state => {
+ 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 };
+ });
+ },
+ clearGuestToSingSongs: () => {
+ set(initialState);
+ },
+ }),
+ {
+ name: GUEST_TO_SING_KEY,
+ storage: createJSONStorage(() => localStorage),
+ },
+ ),
+);
+
+export default useGuestToSingStore;
diff --git a/apps/web/src/types/apiRoute.ts b/apps/web/src/types/apiRoute.ts
index 239cc839..1de85126 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 e7549fd6..37d1aa45 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
@@ -220,6 +220,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
+ framer-motion:
+ specifier: ^12.33.0
+ version: 12.33.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
gsap:
specifier: ^3.14.2
version: 3.14.2
@@ -4564,8 +4567,8 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
- framer-motion@12.29.0:
- resolution: {integrity: sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg==}
+ framer-motion@12.33.0:
+ resolution: {integrity: sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -5836,11 +5839,11 @@ packages:
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
- motion-dom@12.29.0:
- resolution: {integrity: sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA==}
+ motion-dom@12.33.0:
+ resolution: {integrity: sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ==}
- motion-utils@12.27.2:
- resolution: {integrity: sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q==}
+ motion-utils@12.29.2:
+ resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
motion@12.29.0:
resolution: {integrity: sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g==}
@@ -12824,10 +12827,10 @@ snapshots:
fraction.js@5.3.4: {}
- framer-motion@12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ framer-motion@12.33.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- motion-dom: 12.29.0
- motion-utils: 12.27.2
+ motion-dom: 12.33.0
+ motion-utils: 12.29.2
tslib: 2.8.1
optionalDependencies:
react: 19.2.3
@@ -14441,15 +14444,15 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
- motion-dom@12.29.0:
+ motion-dom@12.33.0:
dependencies:
- motion-utils: 12.27.2
+ motion-utils: 12.29.2
- motion-utils@12.27.2: {}
+ motion-utils@12.29.2: {}
motion@12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- framer-motion: 12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ framer-motion: 12.33.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tslib: 2.8.1
optionalDependencies:
react: 19.2.3