diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index d31762a..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/prettierrc", - "tabWidth": 2, - "singleQuote": true, - "printWidth": 120, - "endOfLine": "auto", - "bracketSpacing": true -} diff --git a/apps/web/.prettierrc.cjs b/apps/web/.prettierrc.js similarity index 90% rename from apps/web/.prettierrc.cjs rename to apps/web/.prettierrc.js index c689c11..b065a61 100644 --- a/apps/web/.prettierrc.cjs +++ b/apps/web/.prettierrc.js @@ -1,4 +1,4 @@ -module.exports = { +export default { printWidth: 100, // 한 줄 최대 길이 tabWidth: 2, // 탭 크기 (스페이스 2칸) singleQuote: true, // 작은따옴표 사용 @@ -6,7 +6,7 @@ module.exports = { arrowParens: 'avoid', // 화살표 함수 괄호 생략 (ex: x => x) bracketSpacing: true, // 중괄호 간격 유지 (ex: { foo: bar }) jsxSingleQuote: false, // JSX에서 작은따옴표 사용 안 함 - endOfLine: 'lf', // 줄바꿈 형식 (LF 고정) + endOfLine: 'auto', importOrder: ['', '^@repo/(.*)$', '^@/(.*)$', '^../(.*)$', '^./(.*)$'], importOrderSeparation: true, importOrderSortSpecifiers: true, diff --git a/apps/web/app/hooks/useAddSongList.ts b/apps/web/app/hooks/useAddSongList.ts deleted file mode 100644 index 34383b3..0000000 --- a/apps/web/app/hooks/useAddSongList.ts +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import useLoadingStore from '@/stores/useLoadingStore'; -import useSongStore from '@/stores/useSongStore'; - -export default function useAddSongList() { - const [activeTab, setActiveTab] = useState('liked'); - - const [songSelected, setSongSelected] = useState([]); - const { startLoading, stopLoading, initialLoading } = useLoadingStore(); - - const { refreshLikedSongs, refreshRecentSongs, postToSingSongs } = useSongStore(); - - const handleApiCall = async (apiCall: () => Promise, onError?: () => void) => { - startLoading(); - try { - const result = await apiCall(); - return result; - } catch (error) { - console.error('API 호출 실패:', error); - if (onError) onError(); - return null; - } finally { - stopLoading(); - } - }; - - const getLikedSongs = async () => { - await handleApiCall(async () => { - refreshLikedSongs(); - }); - }; - - const getRecentSongs = async () => { - await handleApiCall(async () => { - refreshRecentSongs(); - }); - }; - - const handleToggleSelect = (songId: string) => { - setSongSelected(prev => - prev.includes(songId) ? prev.filter(id => id !== songId) : [...prev, songId], - ); - }; - - const handleConfirmAdd = async () => { - await handleApiCall(async () => { - await postToSingSongs(songSelected); - setSongSelected([]); - }); - }; - - const totalSelectedCount = songSelected.length; - - useEffect(() => { - getLikedSongs(); - getRecentSongs(); - initialLoading(); - }, []); - - return { - activeTab, - setActiveTab, - songSelected, - handleToggleSelect, - handleConfirmAdd, - totalSelectedCount, - }; -} diff --git a/apps/web/app/hooks/useSearch.ts b/apps/web/app/hooks/useSearch.ts deleted file mode 100644 index 609c871..0000000 --- a/apps/web/app/hooks/useSearch.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { deleteLikedSongs, postLikedSongs } from '@/lib/api/like_activites'; -import { getSearch } from '@/lib/api/search'; -import { deleteToSingSongs, postToSingSongs } from '@/lib/api/tosings'; -import { postTotalStats } from '@/lib/api/total_stats'; -import useLoadingStore from '@/stores/useLoadingStore'; -import { Method } from '@/types/common'; -import { SearchSong } from '@/types/song'; - -type SearchType = 'title' | 'artist'; - -export default function useSearch() { - const [search, setSearch] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [searchType, setSearchType] = useState('title'); - const { startLoading, stopLoading, initialLoading } = useLoadingStore(); - const [isModal, setIsModal] = useState(false); - const [selectedSong, setSelectedSong] = useState(null); - - const handleApiCall = async (apiCall: () => Promise, onError?: () => void) => { - startLoading(); - try { - const result = await apiCall(); - return result; - } catch (error) { - console.error('API 호출 실패:', error); - if (onError) onError(); - return null; - } finally { - stopLoading(); - } - }; - - const handleSearchTypeChange = (value: string) => { - setSearchType(value as SearchType); - }; - - const handleSearch = async () => { - if (!search) return; - - await handleApiCall( - async () => { - const { success, data } = await getSearch(search, searchType); - if (success) { - setSearchResults(data); - } else { - setSearchResults([]); - } - return success; - }, - () => { - setSearchResults([]); - }, - ); - }; - - const handleToggleToSing = async (songId: string, method: Method) => { - await handleApiCall(async () => { - let response; - if (method === 'POST') { - response = await postToSingSongs({ songId }); - } else { - response = await deleteToSingSongs({ songId }); - } - - const { success } = response; - if (success) { - const newResults = searchResults.map(song => { - if (song.id === songId) { - return { ...song, isToSing: !song.isToSing }; - } - return song; - }); - setSearchResults(newResults); - } else { - handleSearch(); - } - return success; - }, handleSearch); - }; - - const handleToggleLike = async (songId: string, method: Method) => { - await handleApiCall(async () => { - await postTotalStats({ - songId, - countType: 'like_count', - isMinus: method === 'DELETE', - }); - - let response; - if (method === 'POST') { - response = await postLikedSongs({ songId }); - } else { - response = await deleteLikedSongs({ songId }); - } - - const { success } = response; - const newResults = searchResults.map(song => { - if (song.id === songId) { - return { ...song, isLiked: !song.isLiked }; - } - return song; - }); - setSearchResults(newResults); - - return success; - }, handleSearch); - }; - - const handleOpenPlaylistModal = (song: SearchSong) => { - setSelectedSong(song); - setIsModal(true); - }; - - const handleSavePlaylist = async () => {}; - - useEffect(() => { - initialLoading(); - }, []); - - return { - search, - setSearch, - searchResults, - searchType, - handleSearchTypeChange, - handleSearch, - handleToggleToSing, - handleToggleLike, - handleOpenPlaylistModal, - isModal, - selectedSong, - handleSavePlaylist, - }; -} diff --git a/apps/web/app/hooks/useSong.ts b/apps/web/app/hooks/useSong.ts deleted file mode 100644 index 70609bf..0000000 --- a/apps/web/app/hooks/useSong.ts +++ /dev/null @@ -1,159 +0,0 @@ -// hooks/useToSingList.ts -import { DragEndEvent } from '@dnd-kit/core'; -import { arrayMove } from '@dnd-kit/sortable'; -import { useEffect } from 'react'; - -import { postSingLog } from '@/lib/api/sing_logs'; -import { patchToSingSongs } from '@/lib/api/tosings'; -import { postTotalStats } from '@/lib/api/total_stats'; -import { postUserStats } from '@/lib/api/user_stats'; -import useAuthStore from '@/stores/useAuthStore'; -import useLoadingStore from '@/stores/useLoadingStore'; -import useSongStore from '@/stores/useSongStore'; - -export default function useSong() { - const { startLoading, stopLoading, initialLoading } = useLoadingStore(); - const { isAuthenticated } = useAuthStore(); - const { toSings, swapToSings, refreshToSings, deleteToSingSong } = useSongStore(); - - const handleApiCall = async (apiCall: () => Promise, onError?: () => void) => { - startLoading(); - try { - const result = await apiCall(); - return result; - } catch (error) { - console.error('API 호출 실패:', error); - if (onError) onError(); - return null; - } finally { - stopLoading(); - } - }; - - const handleSearch = async () => { - await handleApiCall(async () => { - refreshToSings(); - }); - }; - - const handleDragEnd = async (event: DragEndEvent) => { - await handleApiCall(async () => { - const { active, over } = event; - - if (!over || active.id === over.id) return; - - const oldIndex = toSings.findIndex(item => item.songs.id === active.id); - const newIndex = toSings.findIndex(item => item.songs.id === over.id); - - if (oldIndex === newIndex) return; - - const newItems = arrayMove(toSings, oldIndex, newIndex); - const prevItem = newItems[newIndex - 1]; - const nextItem = newItems[newIndex + 1]; - - let newWeight; - - if (!prevItem && nextItem) { - // 제일 앞으로 이동한 경우 - newWeight = toSings[0].order_weight - 1; - } else if (prevItem && !nextItem) { - // 제일 뒤로 이동한 경우 - newWeight = toSings[toSings.length - 1].order_weight + 1; - } else { - // 중간에 삽입 - newWeight = (prevItem.order_weight + nextItem.order_weight) / 2; - } - - const response = await patchToSingSongs({ - songId: active.id as string, - newWeight, - }); - const { success } = response; - swapToSings(newItems); - return success; - }, handleSearch); - }; - - const handleDelete = async (songId: string) => { - await handleApiCall(async () => { - await deleteToSingSong(songId); - swapToSings(toSings.filter(item => item.songs.id !== songId)); - // await fetch('/api/songs/tosing/array', { - // method: 'DELETE', - // body: JSON.stringify({ songIds }), - // headers: { 'Content-Type': 'application/json' }, - // }); - // swapToSings(toSings.filter(item => !songIds.includes(item.songs.id))); - // refreshLikedSongs(); - // refreshRecentSongs(); - }); - }; - - const handleMoveToTop = async (songId: string, oldIndex: number) => { - if (oldIndex === 0) return; - - await handleApiCall(async () => { - const newItems = arrayMove(toSings, oldIndex, 0); - const newWeight = toSings[0].order_weight - 1; - - const response = await patchToSingSongs({ - songId: songId, - newWeight, - }); - const { success } = response; - swapToSings(newItems); - return success; - }, handleSearch); - }; - - const handleMoveToBottom = async (songId: string, oldIndex: number) => { - const lastIndex = toSings.length - 1; - if (oldIndex === lastIndex) return; - - await handleApiCall(async () => { - const newItems = arrayMove(toSings, oldIndex, lastIndex); - const newWeight = toSings[lastIndex].order_weight + 1; - - const response = await patchToSingSongs({ - songId: songId, - newWeight, - }); - const { success } = response; - swapToSings(newItems); - return success; - }, handleSearch); - }; - - const handleSung = async (songId: string) => { - await handleApiCall(async () => { - // 순서 이동 - const oldIndex = toSings.findIndex(item => item.songs.id === songId); - await handleMoveToBottom(songId, oldIndex); - - // 통계 업데이트 - await Promise.all([ - postTotalStats({ songId, countType: 'sing_count', isMinus: false }), - postUserStats(songId), - postSingLog(songId), - handleDelete(songId), - ]); - }, handleSearch); - }; - - // 초기 데이터 로드 - useEffect(() => { - if (isAuthenticated) { - handleSearch(); - initialLoading(); - } - }, [isAuthenticated]); - - return { - handleDragEnd, - handleSearch, - handleDelete, - handleMoveToTop, - handleMoveToBottom, - handleSung, - }; -} diff --git a/apps/web/app/hooks/useSongInfo.ts b/apps/web/app/hooks/useSongInfo.ts deleted file mode 100644 index 23f2acb..0000000 --- a/apps/web/app/hooks/useSongInfo.ts +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import useLoadingStore from '@/stores/useLoadingStore'; -import useSongStore from '@/stores/useSongStore'; - -export default function useAddSongList() { - const [deleteLikeSelected, setDeleteLikeSelected] = useState([]); - const { startLoading, stopLoading, initialLoading } = useLoadingStore(); - - const { refreshLikedSongs, refreshRecentSongs, deleteLikedSongs } = useSongStore(); - - const handleApiCall = async (apiCall: () => Promise, onError?: () => void) => { - startLoading(); - try { - const result = await apiCall(); - return result; - } catch (error) { - console.error('API 호출 실패:', error); - if (onError) onError(); - return null; - } finally { - stopLoading(); - } - }; - - const getLikedSongs = async () => { - await handleApiCall(async () => { - refreshLikedSongs(); - }); - }; - - const getRecentSongs = async () => { - await handleApiCall(async () => { - refreshRecentSongs(); - }); - }; - - const handleToggleSelect = (songId: string) => { - setDeleteLikeSelected(prev => - prev.includes(songId) ? prev.filter(id => id !== songId) : [...prev, songId], - ); - }; - - const handleDelete = async () => { - await handleApiCall(async () => { - await deleteLikedSongs(deleteLikeSelected); - setDeleteLikeSelected([]); - }); - }; - - const totalSelectedCount = deleteLikeSelected.length; - - useEffect(() => { - getLikedSongs(); - getRecentSongs(); - initialLoading(); - }, []); - - return { - deleteLikeSelected, - totalSelectedCount, - handleToggleSelect, - handleDelete, - }; -} diff --git a/apps/web/app/hooks/useUserStat.ts b/apps/web/app/hooks/useUserStat.ts deleted file mode 100644 index 56d334a..0000000 --- a/apps/web/app/hooks/useUserStat.ts +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import { getUserStats } from '@/lib/api/user_stats'; -import { UserSongStat } from '@/types/userStat'; - -export default function useUserStat() { - const [userStat, setUserStat] = useState([]); - - const getUserStat = async () => { - const { success, data } = await getUserStats(); - if (success) { - setUserStat(data); - } - }; - - useEffect(() => { - getUserStat(); - }, []); - - return { userStat, getUserStat }; -} diff --git a/apps/web/app/lib/api/like_activites.ts b/apps/web/app/lib/api/like_activites.ts deleted file mode 100644 index c40ff24..0000000 --- a/apps/web/app/lib/api/like_activites.ts +++ /dev/null @@ -1,42 +0,0 @@ -export async function getLikedSongs() { - const response = await fetch('/api/songs/like'); - if (!response.ok) { - throw new Error('Failed to fetch liked songs'); - } - return response.json(); -} -export async function postLikedSongs(body: { songId: string }) { - const response = await fetch('/api/songs/like', { - method: 'POST', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to post liked songs'); - } - return response.json(); -} - -export async function deleteLikedSongs(body: { songId: string }) { - const response = await fetch('/api/songs/like', { - method: 'DELETE', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to delete liked songs'); - } - return response.json(); -} - -export async function deleteLikedSongsArray(body: { songIds: string[] }) { - const response = await fetch('/api/songs/like/array', { - method: 'DELETE', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to delete liked songs array'); - } - return response.json(); -} diff --git a/apps/web/app/lib/api/search.ts b/apps/web/app/lib/api/search.ts deleted file mode 100644 index 936a7c9..0000000 --- a/apps/web/app/lib/api/search.ts +++ /dev/null @@ -1,7 +0,0 @@ -export async function getSearch(search: string, searchType: string) { - const response = await fetch(`/api/search?q=${search}&type=${searchType}`); - if (!response.ok) { - throw new Error('Failed to search songs'); - } - return response.json(); -} diff --git a/apps/web/app/lib/api/sing_logs.ts b/apps/web/app/lib/api/sing_logs.ts deleted file mode 100644 index f49fc05..0000000 --- a/apps/web/app/lib/api/sing_logs.ts +++ /dev/null @@ -1,11 +0,0 @@ -export async function postSingLog(songId: string) { - const response = await fetch(`/api/sing_logs`, { - method: 'POST', - body: JSON.stringify({ songId }), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to post sing log'); - } - return response.json(); -} diff --git a/apps/web/app/lib/api/songs.ts b/apps/web/app/lib/api/songs.ts deleted file mode 100644 index 52ae7ba..0000000 --- a/apps/web/app/lib/api/songs.ts +++ /dev/null @@ -1,7 +0,0 @@ -export async function getRecentSongs() { - const response = await fetch('/api/songs/recent'); - if (!response.ok) { - throw new Error('Failed to fetch recent songs'); - } - return response.json(); -} diff --git a/apps/web/app/lib/api/tosings.ts b/apps/web/app/lib/api/tosings.ts deleted file mode 100644 index def6267..0000000 --- a/apps/web/app/lib/api/tosings.ts +++ /dev/null @@ -1,67 +0,0 @@ -export async function getToSingSongs() { - const response = await fetch(`/api/songs/tosing`); - if (!response.ok) { - throw new Error('Failed to fetch tosing songs'); - } - return response.json(); -} - -export async function patchToSingSongs(body: { songId: string; newWeight: number }) { - const response = await fetch(`/api/songs/tosing`, { - method: 'PATCH', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to patch tosing songs'); - } - return response.json(); -} - -export async function postToSingSongs(body: { songId: string }) { - const response = await fetch(`/api/songs/tosing`, { - method: 'POST', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to post tosing songs'); - } - return response.json(); -} - -export async function postToSingSongsArray(body: { songIds: string[] }) { - const response = await fetch(`/api/songs/tosing/array`, { - method: 'POST', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to post tosing songs array'); - } - return response.json(); -} - -export async function deleteToSingSongs(body: { songId: string }) { - const response = await fetch(`/api/songs/tosing`, { - method: 'DELETE', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to delete tosing songs'); - } - return response.json(); -} - -export async function deleteToSingSongsArray(body: { songIds: string[] }) { - const response = await fetch(`/api/songs/tosing/array`, { - method: 'DELETE', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to delete tosing songs array'); - } - return response.json(); -} diff --git a/apps/web/app/lib/api/total_stats.ts b/apps/web/app/lib/api/total_stats.ts deleted file mode 100644 index 5db68e7..0000000 --- a/apps/web/app/lib/api/total_stats.ts +++ /dev/null @@ -1,15 +0,0 @@ -export async function postTotalStats(body: { - songId: string; - countType: string; - isMinus: boolean; -}) { - const response = await fetch(`/api/total_stats`, { - method: 'POST', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to post total stats'); - } - return response.json(); -} diff --git a/apps/web/app/lib/api/user_stats.ts b/apps/web/app/lib/api/user_stats.ts deleted file mode 100644 index acd0032..0000000 --- a/apps/web/app/lib/api/user_stats.ts +++ /dev/null @@ -1,19 +0,0 @@ -export async function getUserStats() { - const response = await fetch(`/api/user_stats`); - if (!response.ok) { - throw new Error('Failed to get user stats'); - } - return response.json(); -} - -export async function postUserStats(songId: string) { - const response = await fetch(`/api/user_stats`, { - method: 'POST', - body: JSON.stringify({ songId }), - headers: { 'Content-Type': 'application/json' }, - }); - if (!response.ok) { - throw new Error('Failed to post user stats'); - } - return response.json(); -} diff --git a/apps/web/app/lib/supabase/client.ts b/apps/web/app/lib/supabase/client.ts deleted file mode 100644 index 62af38f..0000000 --- a/apps/web/app/lib/supabase/client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createBrowserClient } from '@supabase/ssr'; - -// Component client - -export default function createClient() { - // CSR에서는 Next_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY 사용 - - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - ); -} diff --git a/apps/web/app/query.tsx b/apps/web/app/query.tsx deleted file mode 100644 index 67799b3..0000000 --- a/apps/web/app/query.tsx +++ /dev/null @@ -1,10 +0,0 @@ -'use client'; - -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useState } from 'react'; - -export default function QueryProvider({ children }: { children: React.ReactNode }) { - const [queryClient] = useState(() => new QueryClient()); - - return {children}; -} diff --git a/apps/web/app/stores/useSongStore.ts b/apps/web/app/stores/useSongStore.ts deleted file mode 100644 index 8ac6b47..0000000 --- a/apps/web/app/stores/useSongStore.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { create } from 'zustand'; - -import { deleteLikedSongsArray, getLikedSongs } from '@/lib/api/like_activites'; -import { getRecentSongs } from '@/lib/api/songs'; -import { deleteToSingSongs, getToSingSongs, postToSingSongsArray } from '@/lib/api/tosings'; -import { AddListModalSong, ToSing } from '@/types/song'; - -interface SongStore { - toSings: ToSing[]; - likedSongs: AddListModalSong[]; - recentSongs: AddListModalSong[]; - swapToSings: (toSings: ToSing[]) => void; - refreshToSings: () => Promise; - refreshLikedSongs: () => Promise; - refreshRecentSongs: () => Promise; - postToSingSongs: (songIds: string[]) => Promise; - deleteToSingSong: (songId: string) => Promise; - deleteLikedSongs: (songIds: string[]) => Promise; -} - -const useSongStore = create((set, get) => ({ - toSings: [], - likedSongs: [], - recentSongs: [], - - swapToSings: (toSings: ToSing[]) => { - set({ toSings }); - }, - - refreshToSings: async () => { - const { success, data } = await getToSingSongs(); - if (success) { - set({ toSings: data }); - } - }, - - refreshLikedSongs: async () => { - const { success, data } = await getLikedSongs(); - if (success) { - set({ likedSongs: data }); - } - }, - refreshRecentSongs: async () => { - const { success, data } = await getRecentSongs(); - if (success) { - set({ recentSongs: data }); - } - }, - - postToSingSongs: async (songIds: string[]) => { - const { success } = await postToSingSongsArray({ songIds }); - if (success) { - get().refreshToSings(); - get().refreshLikedSongs(); - get().refreshRecentSongs(); - } - }, - - deleteToSingSong: async (songId: string) => { - const { success } = await deleteToSingSongs({ songId }); - if (success) { - get().refreshToSings(); - get().refreshLikedSongs(); - get().refreshRecentSongs(); - } - }, - - deleteLikedSongs: async (songIds: string[]) => { - const { success } = await deleteLikedSongsArray({ songIds }); - if (success) { - get().refreshLikedSongs(); - } - }, -})); - -export default useSongStore; diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 0000000..b74e34e --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,13 @@ +// import { dirname } from 'path' +// import { fileURLToPath } from 'url' +// import { FlatCompat } from '@eslint/eslintrc' +// const __filename = fileURLToPath(import.meta.url) +// const __dirname = dirname(__filename) +// const compat = new FlatCompat({ +// baseDirectory: __dirname, +// }) +// const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')] +// export default eslintConfig +import { nextJsConfig } from '@repo/eslint-config/next-js'; + +export default nextJsConfig; diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs deleted file mode 100644 index 46f02ae..0000000 --- a/apps/web/eslint.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { dirname } from 'path' -import { fileURLToPath } from 'url' -import { FlatCompat } from '@eslint/eslintrc' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}) - -const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')] - -export default eslintConfig diff --git a/apps/web/package.json b/apps/web/package.json index cc83b5f..35d4128 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -46,18 +46,14 @@ "zustand": "^5.0.3" }, "devDependencies": { - "@eslint/eslintrc": "^3", + "@repo/eslint-config": "workspace:*", + "@repo/format-config": "workspace:*", "@tailwindcss/postcss": "^4.0.15", - "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "autoprefixer": "^10.4.21", - "eslint": "^9", - "eslint-config-next": "15.2.2", "postcss": "^8.5.3", - "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^4.0.15", "typescript": "^5" }, diff --git a/apps/web/public/github_mark.svg b/apps/web/public/github_mark.svg new file mode 100644 index 0000000..37fa923 --- /dev/null +++ b/apps/web/public/github_mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/app/ErrorWrapper.tsx b/apps/web/src/ErrorWrapper.tsx similarity index 100% rename from apps/web/app/ErrorWrapper.tsx rename to apps/web/src/ErrorWrapper.tsx diff --git a/apps/web/app/Footer.tsx b/apps/web/src/Footer.tsx similarity index 100% rename from apps/web/app/Footer.tsx rename to apps/web/src/Footer.tsx diff --git a/apps/web/app/Header.tsx b/apps/web/src/Header.tsx similarity index 100% rename from apps/web/app/Header.tsx rename to apps/web/src/Header.tsx diff --git a/apps/web/app/Sidebar.tsx b/apps/web/src/Sidebar.tsx similarity index 86% rename from apps/web/app/Sidebar.tsx rename to apps/web/src/Sidebar.tsx index 0789bf6..8a3933f 100644 --- a/apps/web/app/Sidebar.tsx +++ b/apps/web/src/Sidebar.tsx @@ -1,10 +1,12 @@ 'use client'; import { LogOut, Mail, Menu, Pencil, User } from 'lucide-react'; +import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Sheet, @@ -16,8 +18,6 @@ import { } from '@/components/ui/sheet'; import useAuthStore from '@/stores/useAuthStore'; -import { Input } from './components/ui/input'; - export default function Sidebar() { // 목업 인증 상태 const { user, isAuthenticated, logout, changeNickname } = useAuthStore(); @@ -124,6 +124,7 @@ export default function Sidebar() { + {isAuthenticated ? ( )} + +
+
+ © 2025 singcode - Released under the MIT License. + +
+
diff --git a/apps/web/app/api/auth/callback/route.ts b/apps/web/src/app/api/auth/callback/route.ts similarity index 100% rename from apps/web/app/api/auth/callback/route.ts rename to apps/web/src/app/api/auth/callback/route.ts diff --git a/apps/web/app/api/auth/confirm.ts b/apps/web/src/app/api/auth/confirm.ts similarity index 100% rename from apps/web/app/api/auth/confirm.ts rename to apps/web/src/app/api/auth/confirm.ts diff --git a/apps/web/app/api/open_songs/[type]/[param]/route.ts b/apps/web/src/app/api/open_songs/[type]/[param]/route.ts similarity index 100% rename from apps/web/app/api/open_songs/[type]/[param]/route.ts rename to apps/web/src/app/api/open_songs/[type]/[param]/route.ts diff --git a/apps/web/app/api/search/route.ts b/apps/web/src/app/api/search/route.ts similarity index 84% rename from apps/web/app/api/search/route.ts rename to apps/web/src/app/api/search/route.ts index 6453a70..c498351 100644 --- a/apps/web/app/api/search/route.ts +++ b/apps/web/src/app/api/search/route.ts @@ -1,15 +1,10 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; import { SearchSong, Song } from '@/types/song'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -// interface ApiResponse { -// success: boolean; -// data?: T; -// error?: string; -// } - interface DBSong extends Song { like_activities: { user_id: string; @@ -18,8 +13,8 @@ interface DBSong extends Song { user_id: string; }[]; } -// export async function GET(request: Request): Promise>> { -export async function GET(request: Request) { + +export async function GET(request: Request): Promise>> { // API KEY 노출을 막기 위해 미들웨어 역할을 할 API ROUTE 활용 try { const { searchParams } = new URL(request.url); @@ -39,17 +34,6 @@ export async function GET(request: Request) { const supabase = await createClient(); const userId = await getAuthenticatedUser(supabase); // userId 가져오기 - // .select( - // ` - // *, - // like_activities!left ( - // user_id - // ), - // tosings!left ( - // user_id - // ) - // `, - // ) const { data, error } = await supabase .from('songs') .select( @@ -93,6 +77,16 @@ export async function GET(request: Request) { data: songs, }); } catch (error) { + if (error instanceof Error && error.cause === 'auth') { + return NextResponse.json( + { + success: false, + error: 'User not authenticated', + }, + { status: 401 }, + ); + } + console.error('Error in search API:', error); return NextResponse.json( { diff --git a/apps/web/app/api/sing_logs/route.ts b/apps/web/src/app/api/sing_logs/route.ts similarity index 85% rename from apps/web/app/api/sing_logs/route.ts rename to apps/web/src/app/api/sing_logs/route.ts index 7bfe041..9a683f2 100644 --- a/apps/web/app/api/sing_logs/route.ts +++ b/apps/web/src/app/api/sing_logs/route.ts @@ -1,17 +1,15 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -export async function POST(request: Request) { +export async function POST(request: Request): Promise>> { try { const supabase = await createClient(); // Supabase 클라이언트 생성 const { songId } = await request.json(); - console.log('POST'); const userId = await getAuthenticatedUser(supabase); // userId 가져오기 - console.log('songId', songId); - console.log('userId', userId); const { error } = await supabase.from('sing_logs').insert({ song_id: songId, user_id: userId, // userId 추가 diff --git a/apps/web/app/api/songs/like/array/route.ts b/apps/web/src/app/api/songs/like/array/route.ts similarity index 85% rename from apps/web/app/api/songs/like/array/route.ts rename to apps/web/src/app/api/songs/like/array/route.ts index a0daf91..9a7f335 100644 --- a/apps/web/app/api/songs/like/array/route.ts +++ b/apps/web/src/app/api/songs/like/array/route.ts @@ -1,9 +1,10 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -export async function DELETE(request: Request) { +export async function DELETE(request: Request): Promise>> { try { const supabase = await createClient(); // Supabase 클라이언트 생성 const userId = await getAuthenticatedUser(supabase); // userId 가져오기 diff --git a/apps/web/app/api/songs/like/route.ts b/apps/web/src/app/api/songs/like/route.ts similarity index 77% rename from apps/web/app/api/songs/like/route.ts rename to apps/web/src/app/api/songs/like/route.ts index 85fb257..7c0ecc0 100644 --- a/apps/web/app/api/songs/like/route.ts +++ b/apps/web/src/app/api/songs/like/route.ts @@ -1,14 +1,15 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; -import { LikeLog } from '@/types/likeLog'; -import { Song } from '@/types/song'; +import { ApiResponse } from '@/types/apiRoute'; +import { PersonalSong, Song } from '@/types/song'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -interface ResponseLikeLog extends LikeLog { +interface ResponseLikeLog extends PersonalSong { songs: Song; } -export async function GET() { + +export async function GET(): Promise>> { try { const supabase = await createClient(); const userId = await getAuthenticatedUser(supabase); @@ -61,6 +62,15 @@ export async function GET() { return NextResponse.json({ success: true, data: processedData }); } catch (error) { + if (error instanceof Error && error.cause === 'auth') { + return NextResponse.json( + { + success: false, + error: 'User not authenticated', + }, + { status: 401 }, + ); + } console.error('Error in like API:', error); return NextResponse.json( { success: false, error: 'Failed to get like songs' }, @@ -69,10 +79,10 @@ export async function GET() { } } -export async function POST(request: Request) { +export async function POST(request: Request): Promise>> { try { - const supabase = await createClient(); // Supabase 클라이언트 생성 - const userId = await getAuthenticatedUser(supabase); // userId 가져오기 + const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); const { songId } = await request.json(); @@ -91,10 +101,11 @@ export async function POST(request: Request) { ); } } -export async function DELETE(request: Request) { + +export async function DELETE(request: Request): Promise>> { try { - const supabase = await createClient(); // Supabase 클라이언트 생성 - const userId = await getAuthenticatedUser(supabase); // userId 가져오기 + const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); const { songId } = await request.json(); diff --git a/apps/web/app/api/songs/recent/route.tsx b/apps/web/src/app/api/songs/recent/route.tsx similarity index 82% rename from apps/web/app/api/songs/recent/route.tsx rename to apps/web/src/app/api/songs/recent/route.tsx index 03c5345..11ffd07 100644 --- a/apps/web/app/api/songs/recent/route.tsx +++ b/apps/web/src/app/api/songs/recent/route.tsx @@ -1,14 +1,14 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; -import { SingLog } from '@/types/singLog'; -import { Song } from '@/types/song'; +import { ApiResponse } from '@/types/apiRoute'; +import { PersonalSong, Song } from '@/types/song'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -interface ResponseSingLog extends SingLog { +interface ResponseSingLog extends PersonalSong { songs: Song; } -export async function GET() { +export async function GET(): Promise>> { try { const supabase = await createClient(); const userId = await getAuthenticatedUser(supabase); @@ -62,6 +62,16 @@ export async function GET() { return NextResponse.json({ success: true, data: processedData }); } catch (error) { + if (error instanceof Error && error.cause === 'auth') { + return NextResponse.json( + { + success: false, + error: 'User not authenticated', + }, + { status: 401 }, + ); + } + console.error('Error in recent API:', error); return NextResponse.json( { success: false, error: 'Failed to get recent songs' }, diff --git a/apps/web/app/api/songs/tosing/array/route.ts b/apps/web/src/app/api/songs/tosing/array/route.ts similarity index 89% rename from apps/web/app/api/songs/tosing/array/route.ts rename to apps/web/src/app/api/songs/tosing/array/route.ts index c6f53eb..522b23a 100644 --- a/apps/web/app/api/songs/tosing/array/route.ts +++ b/apps/web/src/app/api/songs/tosing/array/route.ts @@ -1,9 +1,10 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -export async function POST(request: Request) { +export async function POST(request: Request): Promise>> { try { const supabase = await createClient(); // Supabase 클라이언트 생성 const userId = await getAuthenticatedUser(supabase); // userId 가져오기 diff --git a/apps/web/app/api/songs/tosing/route.ts b/apps/web/src/app/api/songs/tosing/route.ts similarity index 54% rename from apps/web/app/api/songs/tosing/route.ts rename to apps/web/src/app/api/songs/tosing/route.ts index 8bacbb5..f6c4fe0 100644 --- a/apps/web/app/api/songs/tosing/route.ts +++ b/apps/web/src/app/api/songs/tosing/route.ts @@ -1,26 +1,28 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; +import { ToSingSong } from '@/types/song'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -export async function GET() { +export async function GET(): Promise>> { try { - const supabase = await createClient(); // Supabase 클라이언트 생성 - const userId = await getAuthenticatedUser(supabase); // userId 가져오기 + const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); const { data, error } = await supabase .from('tosings') .select( ` - order_weight, - songs ( - id, - title, - artist, - num_tj, - num_ky - ) -`, + order_weight, + songs ( + id, + title, + artist, + num_tj, + num_ky + ) + `, ) .eq('user_id', userId) .order('order_weight'); @@ -35,20 +37,31 @@ export async function GET() { ); } - return NextResponse.json({ success: true, data }); + return NextResponse.json({ success: true, data: data as unknown as ToSingSong[] }); } catch (error) { - console.error('Error in tosings API:', error); + if (error instanceof Error && error.cause === 'auth') { + return NextResponse.json( + { + success: false, + error: 'User not authenticated', + }, + { status: 401 }, + ); + } + + console.error('Error in tosing API:', error); return NextResponse.json( - { success: false, error: 'Failed to get tosings song' }, + { success: false, error: 'Failed to get tosing songs' }, { status: 500 }, ); } } -export async function POST(request: Request) { +export async function POST(request: Request): Promise>> { try { - const supabase = await createClient(); // Supabase 클라이언트 생성 - const userId = await getAuthenticatedUser(supabase); // userId 가져오기 + const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); + const { songId } = await request.json(); const { data: maxRow, error: maxError } = await supabase @@ -71,18 +84,19 @@ export async function POST(request: Request) { return NextResponse.json({ success: true }); } catch (error) { - console.error('Error in tosings API:', error); + console.error('Error in tosing API:', error); return NextResponse.json( - { success: false, error: 'Failed to post tosings song' }, + { success: false, error: 'Failed to post tosing song' }, { status: 500 }, ); } } -export async function PATCH(request: Request) { +export async function PATCH(request: Request): Promise>> { try { - const supabase = await createClient(); // Supabase 클라이언트 생성 - const userId = await getAuthenticatedUser(supabase); // userId 가져오기 + const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); + const { songId, newWeight } = await request.json(); if (!songId || newWeight === undefined) { @@ -96,37 +110,38 @@ export async function PATCH(request: Request) { .from('tosings') .update({ order_weight: newWeight }) .match({ user_id: userId, song_id: songId }); + if (error) throw error; return NextResponse.json({ success: true }); } catch (error) { - console.error('Error in tosings API:', error); + console.error('Error in tosing API:', error); return NextResponse.json( - { success: false, error: 'Failed to patch tosings song' }, + { success: false, error: 'Failed to patch tosing song' }, { status: 500 }, ); } } -export async function DELETE(request: Request) { +export async function DELETE(request: Request): Promise>> { try { - const supabase = await createClient(); // Supabase 클라이언트 생성 - const userId = await getAuthenticatedUser(supabase); // userId 가져오기 + const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); + const { songId } = await request.json(); const { error } = await supabase .from('tosings') .delete() - .match({ user_id: userId, song_id: songId }) - .select('*'); // 삭제된 row의 sequence 값을 가져오기 위해 select('*') 추가 + .match({ user_id: userId, song_id: songId }); if (error) throw error; return NextResponse.json({ success: true }); } catch (error) { - console.error('Error in tosings API:', error); + console.error('Error in tosing API:', error); return NextResponse.json( - { success: false, error: 'Failed to delete tosings song' }, + { success: false, error: 'Failed to delete tosing song' }, { status: 500 }, ); } diff --git a/apps/web/app/api/total_stats/route.ts b/apps/web/src/app/api/total_stats/route.ts similarity index 92% rename from apps/web/app/api/total_stats/route.ts rename to apps/web/src/app/api/total_stats/route.ts index 852b223..a3db280 100644 --- a/apps/web/app/api/total_stats/route.ts +++ b/apps/web/src/app/api/total_stats/route.ts @@ -1,11 +1,12 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; // 유효한 카운트 타입 정의 type CountType = 'sing_count' | 'like_count' | 'saved_count'; -export async function POST(request: Request) { +export async function POST(request: Request): Promise>> { try { const supabase = await createClient(); const { songId, countType, isMinus } = await request.json(); diff --git a/apps/web/app/api/user_stats/route.ts b/apps/web/src/app/api/user_stats/route.ts similarity index 81% rename from apps/web/app/api/user_stats/route.ts rename to apps/web/src/app/api/user_stats/route.ts index 6014711..39505c6 100644 --- a/apps/web/app/api/user_stats/route.ts +++ b/apps/web/src/app/api/user_stats/route.ts @@ -1,9 +1,11 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; +import { ApiResponse } from '@/types/apiRoute'; +import { UserSongStat } from '@/types/userStat'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; -export async function GET() { +export async function GET(): Promise>> { const supabase = await createClient(); const userId = await getAuthenticatedUser(supabase); @@ -27,7 +29,7 @@ export async function GET() { return NextResponse.json({ success: true, data: parsedData }); } -export async function POST(request: Request) { +export async function POST(request: Request): Promise>> { try { const supabase = await createClient(); // Supabase 클라이언트 생성 const { songId } = await request.json(); @@ -66,6 +68,16 @@ export async function POST(request: Request) { return NextResponse.json({ success: true }); } catch (error) { + if (error instanceof Error && error.cause === 'auth') { + return NextResponse.json( + { + success: false, + error: 'User not authenticated', + }, + { status: 401 }, + ); + } + console.error('Error in tosings API:', error); return NextResponse.json( { success: false, error: 'Failed to post user_stats' }, diff --git a/apps/web/app/error.tsx b/apps/web/src/app/error.tsx similarity index 96% rename from apps/web/app/error.tsx rename to apps/web/src/app/error.tsx index 7fd9a80..4614a20 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/src/app/error.tsx @@ -1,7 +1,6 @@ 'use client'; import { AlertCircle, Home } from 'lucide-react'; -import { useEffect } from 'react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; @@ -39,9 +38,9 @@ export default function Error({ error }: ErrorPageProps) { } // 에러 로깅 - useEffect(() => { - console.error('페이지 에러:', error); - }, [error]); + // useEffect(() => { + // console.error('페이지 에러:', error); + // }, [error]); return (
diff --git a/apps/web/app/error/page.tsx b/apps/web/src/app/error/page.tsx similarity index 100% rename from apps/web/app/error/page.tsx rename to apps/web/src/app/error/page.tsx diff --git a/apps/web/app/home/AddListModal.tsx b/apps/web/src/app/home/AddListModal.tsx similarity index 86% rename from apps/web/app/home/AddListModal.tsx rename to apps/web/src/app/home/AddListModal.tsx index 2969556..422fd72 100644 --- a/apps/web/app/home/AddListModal.tsx +++ b/apps/web/src/app/home/AddListModal.tsx @@ -10,7 +10,8 @@ import { } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import useAddListModal from '@/hooks/useAddSongList'; -import useSongStore from '@/stores/useSongStore'; +import { useLikeSongQuery } from '@/queries/likeSongQuery'; +import { useRecentSongsQuery } from '@/queries/recentSongQuery'; import ModalSongItem from './ModalSongItem'; @@ -29,7 +30,8 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) { totalSelectedCount, } = useAddListModal(); - const { likedSongs, recentSongs } = useSongStore(); + const { data: likedSongs } = useLikeSongQuery(); + const { data: recentSongs } = useRecentSongsQuery(); const handleClickConfirm = () => { handleConfirmAdd(); @@ -60,9 +62,9 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) {
{likedSongs && - likedSongs.map(song => ( + likedSongs.map((song, index) => (
{recentSongs && - recentSongs.map(song => ( + recentSongs.map((song, index) => ( ; + return ( item.songs.id)} + items={toSingSongs.map((item: ToSingSong) => item.songs.id)} strategy={verticalListSortingStrategy} >
- {isInitialLoading && ( -
- -
- )} - {!isInitialLoading && toSings.length === 0 && ( + {toSingSongs.length === 0 && (

노래방 플레이리스트가 없습니다.

)} - {toSings.map((item: ToSing, index: number) => ( + {toSingSongs.map((item: ToSingSong, index: number) => ( - - - + + +
{children}
@@ -64,9 +63,9 @@ export default function RootLayout({ - - - + + + ); diff --git a/apps/web/app/library/liked/SongItem.tsx b/apps/web/src/app/library/liked/SongItem.tsx similarity index 88% rename from apps/web/app/library/liked/SongItem.tsx rename to apps/web/src/app/library/liked/SongItem.tsx index 010380d..af810c2 100644 --- a/apps/web/app/library/liked/SongItem.tsx +++ b/apps/web/src/app/library/liked/SongItem.tsx @@ -15,9 +15,9 @@ export default function SongItem({ return (
onToggleSelect(song.id)} + onCheckedChange={() => onToggleSelect(song.song_id)} />

{song.title}

diff --git a/apps/web/app/library/liked/page.tsx b/apps/web/src/app/library/liked/page.tsx similarity index 76% rename from apps/web/app/library/liked/page.tsx rename to apps/web/src/app/library/liked/page.tsx index 0019929..e708d44 100644 --- a/apps/web/app/library/liked/page.tsx +++ b/apps/web/src/app/library/liked/page.tsx @@ -3,21 +3,24 @@ import { ArrowLeft } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import StaticLoading from '@/components/StaticLoading'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import useSongInfo from '@/hooks/useSongInfo'; -import useSongStore from '@/stores/useSongStore'; +import { useLikeSongQuery } from '@/queries/likeSongQuery'; import SongItem from './SongItem'; export default function LikedPage() { const router = useRouter(); - const { likedSongs } = useSongStore(); - const { deleteLikeSelected, handleToggleSelect, handleDelete } = useSongInfo(); + const { data, isLoading } = useLikeSongQuery(); + const { deleteLikeSelected, handleToggleSelect, handleDeleteArray } = useSongInfo(); + const likedSongs = data ?? []; return (
+ {isLoading && }
)} @@ -43,9 +46,9 @@ export default function LikedPage() { {likedSongs.map(song => ( ))} diff --git a/apps/web/app/library/page.tsx b/apps/web/src/app/library/page.tsx similarity index 91% rename from apps/web/app/library/page.tsx rename to apps/web/src/app/library/page.tsx index 79d850c..d32bccb 100644 --- a/apps/web/app/library/page.tsx +++ b/apps/web/src/app/library/page.tsx @@ -24,10 +24,11 @@ const menuItems = [ export default function LibraryPage() { const router = useRouter(); const { user } = useAuthStore(); + const nickname = user?.nickname ?? '근데 누구셨더라...?'; return (
-

반갑습니다, {user?.nickname}님

+

반가워요, {nickname}

{menuItems.map(item => ( + {isLoading && }
- {searchResults.length > 0 ? ( + {searchSongs.length > 0 && (
- {searchResults.map((song, index) => ( + {searchSongs.map((song, index) => ( ))}
- ) : ( + )} + {searchSongs.length === 0 && query && ( +
+ +

검색 결과가 없습니다.

+
+ )} + {searchSongs.length === 0 && !query && (
-

노래 제목이나 가수를 검색해보세요

+

노래 제목이나 가수를 검색해보세요

)}
+ {isLoading && } {/* {isModal && } */}
); diff --git a/apps/web/app/signup/actions.ts b/apps/web/src/app/signup/actions.ts similarity index 100% rename from apps/web/app/signup/actions.ts rename to apps/web/src/app/signup/actions.ts diff --git a/apps/web/app/signup/page.tsx b/apps/web/src/app/signup/page.tsx similarity index 100% rename from apps/web/app/signup/page.tsx rename to apps/web/src/app/signup/page.tsx diff --git a/apps/web/app/update-password/page.tsx b/apps/web/src/app/update-password/page.tsx similarity index 99% rename from apps/web/app/update-password/page.tsx rename to apps/web/src/app/update-password/page.tsx index e07b99c..8eaf08b 100644 --- a/apps/web/app/update-password/page.tsx +++ b/apps/web/src/app/update-password/page.tsx @@ -27,6 +27,7 @@ export default function UpdatePasswordPage() { const { openMessage } = useModalStore(); const router = useRouter(); + const supabase = createClient(); // 이메일 제출 처리 (비밀번호 재설정 링크 요청) const handleSendResetLink = async (e: React.FormEvent) => { @@ -64,7 +65,7 @@ export default function UpdatePasswordPage() { }; useEffect(() => { - const supabase = createClient(); + if (!supabase) return; // 현재 세션 상태 확인 const checkCurrentSession = async () => { @@ -99,7 +100,7 @@ export default function UpdatePasswordPage() { return () => { subscription.unsubscribe(); }; - }, []); + }, [supabase]); return (
diff --git a/apps/web/app/auth.tsx b/apps/web/src/auth.tsx similarity index 100% rename from apps/web/app/auth.tsx rename to apps/web/src/auth.tsx diff --git a/apps/web/app/components/ErrorContent.tsx b/apps/web/src/components/ErrorContent.tsx similarity index 100% rename from apps/web/app/components/ErrorContent.tsx rename to apps/web/src/components/ErrorContent.tsx diff --git a/apps/web/app/components/LoadingOverlay.tsx b/apps/web/src/components/LoadingOverlay.tsx similarity index 100% rename from apps/web/app/components/LoadingOverlay.tsx rename to apps/web/src/components/LoadingOverlay.tsx diff --git a/apps/web/app/components/RankingList.tsx b/apps/web/src/components/RankingList.tsx similarity index 100% rename from apps/web/app/components/RankingList.tsx rename to apps/web/src/components/RankingList.tsx diff --git a/apps/web/src/components/StaticLoading.tsx b/apps/web/src/components/StaticLoading.tsx new file mode 100644 index 0000000..d186248 --- /dev/null +++ b/apps/web/src/components/StaticLoading.tsx @@ -0,0 +1,10 @@ +import { Loader2 } from 'lucide-react'; + +export default function StaticLoading() { + return ( +
+ {/*
*/} + +
+ ); +} diff --git a/apps/web/app/components/messageDialog.tsx b/apps/web/src/components/messageDialog.tsx similarity index 100% rename from apps/web/app/components/messageDialog.tsx rename to apps/web/src/components/messageDialog.tsx diff --git a/apps/web/app/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx similarity index 100% rename from apps/web/app/components/ui/alert.tsx rename to apps/web/src/components/ui/alert.tsx diff --git a/apps/web/app/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx similarity index 100% rename from apps/web/app/components/ui/button.tsx rename to apps/web/src/components/ui/button.tsx diff --git a/apps/web/app/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx similarity index 100% rename from apps/web/app/components/ui/card.tsx rename to apps/web/src/components/ui/card.tsx diff --git a/apps/web/app/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx similarity index 100% rename from apps/web/app/components/ui/checkbox.tsx rename to apps/web/src/components/ui/checkbox.tsx diff --git a/apps/web/app/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx similarity index 100% rename from apps/web/app/components/ui/dialog.tsx rename to apps/web/src/components/ui/dialog.tsx diff --git a/apps/web/app/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx similarity index 100% rename from apps/web/app/components/ui/dropdown-menu.tsx rename to apps/web/src/components/ui/dropdown-menu.tsx diff --git a/apps/web/app/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx similarity index 100% rename from apps/web/app/components/ui/input.tsx rename to apps/web/src/components/ui/input.tsx diff --git a/apps/web/app/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx similarity index 100% rename from apps/web/app/components/ui/label.tsx rename to apps/web/src/components/ui/label.tsx diff --git a/apps/web/app/components/ui/scroll-area.tsx b/apps/web/src/components/ui/scroll-area.tsx similarity index 100% rename from apps/web/app/components/ui/scroll-area.tsx rename to apps/web/src/components/ui/scroll-area.tsx diff --git a/apps/web/app/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx similarity index 100% rename from apps/web/app/components/ui/separator.tsx rename to apps/web/src/components/ui/separator.tsx diff --git a/apps/web/app/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx similarity index 100% rename from apps/web/app/components/ui/sheet.tsx rename to apps/web/src/components/ui/sheet.tsx diff --git a/apps/web/app/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx similarity index 100% rename from apps/web/app/components/ui/sonner.tsx rename to apps/web/src/components/ui/sonner.tsx diff --git a/apps/web/app/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx similarity index 100% rename from apps/web/app/components/ui/tabs.tsx rename to apps/web/src/components/ui/tabs.tsx diff --git a/apps/web/app/globals.css b/apps/web/src/globals.css similarity index 100% rename from apps/web/app/globals.css rename to apps/web/src/globals.css diff --git a/apps/web/src/hooks/useAddSongList.ts b/apps/web/src/hooks/useAddSongList.ts new file mode 100644 index 0000000..44e555e --- /dev/null +++ b/apps/web/src/hooks/useAddSongList.ts @@ -0,0 +1,35 @@ +'use client'; + +import { useState } from 'react'; + +import { usePostToSingSongMutation } from '@/queries/tosingSongQuery'; + +export default function useAddSongList() { + const [activeTab, setActiveTab] = useState('liked'); + + const [songSelected, setSongSelected] = useState([]); + + const { mutate: postToSingSong } = usePostToSingSongMutation(); + + const handleToggleSelect = (songId: string) => { + setSongSelected(prev => + prev.includes(songId) ? prev.filter(id => id !== songId) : [...prev, songId], + ); + }; + + const handleConfirmAdd = () => { + postToSingSong(songSelected); + setSongSelected([]); + }; + + const totalSelectedCount = songSelected.length; + + return { + activeTab, + setActiveTab, + songSelected, + handleToggleSelect, + handleConfirmAdd, + totalSelectedCount, + }; +} diff --git a/apps/web/src/hooks/useSearchSong.ts b/apps/web/src/hooks/useSearchSong.ts new file mode 100644 index 0000000..0be5a7d --- /dev/null +++ b/apps/web/src/hooks/useSearchSong.ts @@ -0,0 +1,65 @@ +import { useState } from 'react'; + +import { + useSearchSongSongQuery, + useToggleLikeMutation, + useToggleToSingMutation, +} from '@/queries/searchSongQuery'; +import { Method } from '@/types/common'; +import { SearchSong } from '@/types/song'; + +type SearchType = 'title' | 'artist'; + +export default function useSearchSong() { + const [search, setSearch] = useState(''); + const [query, setQuery] = useState(''); + const [searchType, setSearchType] = useState('title'); + const [isModal, setIsModal] = useState(false); + const [selectedSong, setSelectedSong] = useState(null); + const { data: searchResults, isLoading } = useSearchSongSongQuery(query, searchType); + const { mutate: toggleToSing } = useToggleToSingMutation(); + const { mutate: toggleLike } = useToggleLikeMutation(); + + const searchSongs = searchResults ?? []; + + const handleSearch = () => { + setQuery(search); + setSearch(''); + }; + + const handleSearchTypeChange = (value: string) => { + setSearchType(value as SearchType); + }; + + const handleToggleToSing = async (songId: string, method: Method) => { + toggleToSing({ songId, method, query, searchType }); + }; + + const handleToggleLike = async (songId: string, method: Method) => { + toggleLike({ songId, method, query, searchType }); + }; + + const handleOpenPlaylistModal = (song: SearchSong) => { + setSelectedSong(song); + setIsModal(true); + }; + + const handleSavePlaylist = async () => {}; + + return { + search, + setSearch, + query, + searchSongs, + isLoading, + searchType, + handleSearchTypeChange, + handleSearch, + handleToggleToSing, + handleToggleLike, + handleOpenPlaylistModal, + isModal, + selectedSong, + handleSavePlaylist, + }; +} diff --git a/apps/web/src/hooks/useSong.ts b/apps/web/src/hooks/useSong.ts new file mode 100644 index 0000000..5a37069 --- /dev/null +++ b/apps/web/src/hooks/useSong.ts @@ -0,0 +1,107 @@ +// hooks/useToSingList.ts +import { DragEndEvent } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; + +import { usePostSingLogMutation } from '@/queries/singLogQuery'; +import { + useDeleteToSingSongMutation, + usePatchToSingSongMutation, + useToSingSongQuery, +} from '@/queries/tosingSongQuery'; + +export default function useSong() { + const { data, isLoading } = useToSingSongQuery(); + const { mutate: patchToSingSong } = usePatchToSingSongMutation(); + const { mutate: deleteToSingSong } = useDeleteToSingSongMutation(); + const { mutate: postSingLog } = usePostSingLogMutation(); + const toSingSongs = data ?? []; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (!over || active.id === over.id) return; + + const oldIndex = toSingSongs.findIndex(item => item.songs.id === active.id); + const newIndex = toSingSongs.findIndex(item => item.songs.id === over.id); + + if (oldIndex === newIndex) return; + + const newItems = arrayMove(toSingSongs, oldIndex, newIndex); + const prevItem = newItems[newIndex - 1]; + const nextItem = newItems[newIndex + 1]; + + let newWeight; + + if (!prevItem && nextItem) { + // 제일 앞으로 이동한 경우 + newWeight = toSingSongs[0].order_weight - 1; + } else if (prevItem && !nextItem) { + // 제일 뒤로 이동한 경우 + newWeight = toSingSongs[toSingSongs.length - 1].order_weight + 1; + } else { + // 중간에 삽입 + newWeight = (prevItem.order_weight + nextItem.order_weight) / 2; + } + + patchToSingSong({ + songId: active.id as string, + newWeight, + newItems, + }); + }; + + const handleDelete = (songId: string) => { + deleteToSingSong(songId); + }; + + const handleMoveToTop = (songId: string, oldIndex: number) => { + if (oldIndex === 0) return; + + const newItems = arrayMove(toSingSongs, oldIndex, 0); + const newWeight = toSingSongs[0].order_weight - 1; + + patchToSingSong({ + songId: songId, + newWeight, + newItems, + }); + }; + + const handleMoveToBottom = (songId: string, oldIndex: number) => { + const lastIndex = toSingSongs.length - 1; + if (oldIndex === lastIndex) return; + + const newItems = arrayMove(toSingSongs, oldIndex, lastIndex); + const newWeight = toSingSongs[lastIndex].order_weight + 1; + + patchToSingSong({ + songId: songId, + newWeight, + newItems, + }); + }; + + const handleSung = async (songId: string) => { + // 순서 이동 + const oldIndex = toSingSongs.findIndex(item => item.songs.id === songId); + handleMoveToBottom(songId, oldIndex); + + // 통계 업데이트 + await Promise.all([ + // postTotalStat({ songId, countType: 'sing_count', isMinus: false }), + // postUserStats(songId), + postSingLog(songId), + handleDelete(songId), + ]); + }; + + return { + toSingSongs, + isLoading, + handleDragEnd, + handleDelete, + handleMoveToTop, + handleMoveToBottom, + handleSung, + }; +} diff --git a/apps/web/src/hooks/useSongInfo.ts b/apps/web/src/hooks/useSongInfo.ts new file mode 100644 index 0000000..4dfa256 --- /dev/null +++ b/apps/web/src/hooks/useSongInfo.ts @@ -0,0 +1,42 @@ +'use client'; + +import { useState } from 'react'; + +import { useDeleteLikeSongArrayMutation } from '@/queries/likeSongQuery'; + +// import { useDeleteLikedSongMutation } from '@/queries/likeSongQuery'; + +export default function useSongInfo() { + const [deleteLikeSelected, setDeleteLikeSelected] = useState([]); + + const { mutate: deleteLikeSongArray } = useDeleteLikeSongArrayMutation(); + // const { mutate: deleteLikeSong } = useDeleteLikedSongMutation(); + + const handleToggleSelect = (songId: string) => { + setDeleteLikeSelected(prev => + prev.includes(songId) ? prev.filter(id => id !== songId) : [...prev, songId], + ); + }; + + const handleDeleteArray = () => { + console.log('deleteLikeSelected', deleteLikeSelected); + deleteLikeSongArray(deleteLikeSelected); + setDeleteLikeSelected([]); + }; + + // const handleDelete = () => { + // deleteLikeSelected.forEach(songId => { + // deleteLikeSong(songId); + // }); + // setDeleteLikeSelected([]); + // }; + + const totalSelectedCount = deleteLikeSelected.length; + + return { + deleteLikeSelected, + totalSelectedCount, + handleToggleSelect, + handleDeleteArray, + }; +} diff --git a/apps/web/src/hooks/useUserStat.ts b/apps/web/src/hooks/useUserStat.ts new file mode 100644 index 0000000..a1ea987 --- /dev/null +++ b/apps/web/src/hooks/useUserStat.ts @@ -0,0 +1,10 @@ +'use client'; + +import { useUserStatQuery } from '@/queries/userStatQuery'; + +export default function useUserStat() { + const { data, isLoading } = useUserStatQuery(); + const userStat = data ?? []; + + return { userStat, isLoading }; +} diff --git a/apps/web/src/lib/api/client.ts b/apps/web/src/lib/api/client.ts new file mode 100644 index 0000000..4b8ea5e --- /dev/null +++ b/apps/web/src/lib/api/client.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; + +export const instance = axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'application/json', + }, + timeout: 5000, // 5초 +}); + +// instance.interceptors.response.use( +// response => response.data, +// (error: AxiosError) => { +// throw new Error(error.response?.data?.error || error.response?.data?.message || '서버 에러가 발생했습니다.'); +// }, +// ); diff --git a/apps/web/src/lib/api/likeSong.ts b/apps/web/src/lib/api/likeSong.ts new file mode 100644 index 0000000..0fcbd49 --- /dev/null +++ b/apps/web/src/lib/api/likeSong.ts @@ -0,0 +1,24 @@ +import { ApiResponse } from '@/types/apiRoute'; +import { PersonalSong } from '@/types/song'; + +import { instance } from './client'; + +export async function getLikeSongs() { + const response = await instance.get>('/songs/like'); + return response.data; +} + +export async function postLikeSong(body: { songId: string }) { + const response = await instance.post>('/songs/like', body); + return response.data; +} + +export async function deleteLikeSong(body: { songId: string }) { + const response = await instance.delete>('/songs/like', { data: body }); + return response.data; +} + +export async function deleteLikeSongArray(body: { songIds: string[] }) { + const response = await instance.delete>('/songs/like/array', { data: body }); + return response.data; +} diff --git a/apps/web/src/lib/api/recentSong.ts b/apps/web/src/lib/api/recentSong.ts new file mode 100644 index 0000000..c14c304 --- /dev/null +++ b/apps/web/src/lib/api/recentSong.ts @@ -0,0 +1,9 @@ +import { ApiResponse } from '@/types/apiRoute'; +import { PersonalSong } from '@/types/song'; + +import { instance } from './client'; + +export async function getRecentSong() { + const response = await instance.get>('/songs/recent'); + return response.data; +} diff --git a/apps/web/src/lib/api/searchSong.ts b/apps/web/src/lib/api/searchSong.ts new file mode 100644 index 0000000..271bf4f --- /dev/null +++ b/apps/web/src/lib/api/searchSong.ts @@ -0,0 +1,12 @@ +import { ApiResponse } from '@/types/apiRoute'; +import { SearchSong } from '@/types/song'; + +import { instance } from './client'; + +export async function getSearchSong(search: string, searchType: string) { + const response = await instance.get>('/search', { + params: { q: search, type: searchType }, + }); + + return response.data; +} diff --git a/apps/web/src/lib/api/singLog.ts b/apps/web/src/lib/api/singLog.ts new file mode 100644 index 0000000..73349c5 --- /dev/null +++ b/apps/web/src/lib/api/singLog.ts @@ -0,0 +1,8 @@ +import { ApiResponse } from '@/types/apiRoute'; + +import { instance } from './client'; + +export async function postSingLog(songId: string) { + const response = await instance.post>('/sing_logs', { songId }); + return response.data; +} diff --git a/apps/web/src/lib/api/tosing.ts b/apps/web/src/lib/api/tosing.ts new file mode 100644 index 0000000..cb20957 --- /dev/null +++ b/apps/web/src/lib/api/tosing.ts @@ -0,0 +1,36 @@ +import { ApiResponse } from '@/types/apiRoute'; +import { ToSingSong } from '@/types/song'; + +import { instance } from './client'; + +export async function getToSingSong() { + const response = await instance.get>('/songs/tosing'); + return response.data; +} + +export async function patchToSingSong(body: { songId: string; newWeight: number }) { + const response = await instance.patch>('/songs/tosing', body); + return response.data; +} + +export async function postToSingSong(body: { songId: string }) { + const response = await instance.post>('/songs/tosing', body); + return response.data; +} + +export async function postToSingSongArray(body: { songIds: string[] }) { + const response = await instance.post>('/songs/tosing/array', body); + return response.data; +} + +export async function deleteToSingSong(body: { songId: string }) { + const response = await instance.delete>('/songs/tosing', { data: body }); + return response.data; +} + +export async function deleteToSingSongArray(body: { songIds: string[] }) { + const response = await instance.delete>('/songs/tosing/array', { + data: body, + }); + return response.data; +} diff --git a/apps/web/src/lib/api/totalStat.ts b/apps/web/src/lib/api/totalStat.ts new file mode 100644 index 0000000..b010195 --- /dev/null +++ b/apps/web/src/lib/api/totalStat.ts @@ -0,0 +1,8 @@ +import { ApiResponse } from '@/types/apiRoute'; + +import { instance } from './client'; + +export async function postTotalStat(body: { songId: string; countType: string; isMinus: boolean }) { + const response = await instance.post>('/total_stats', body); + return response.data; +} diff --git a/apps/web/src/lib/api/userStat.ts b/apps/web/src/lib/api/userStat.ts new file mode 100644 index 0000000..f794538 --- /dev/null +++ b/apps/web/src/lib/api/userStat.ts @@ -0,0 +1,14 @@ +import { ApiResponse } from '@/types/apiRoute'; +import { UserSongStat } from '@/types/userStat'; + +import { instance } from './client'; + +export async function getUserStats() { + const response = await instance.get>('/user_stats'); + return response.data; +} + +export async function postUserStats(songId: string) { + const response = await instance.post>('/user_stats', { songId }); + return response.data; +} diff --git a/apps/web/app/lib/supabase/api.ts b/apps/web/src/lib/supabase/api.ts similarity index 100% rename from apps/web/app/lib/supabase/api.ts rename to apps/web/src/lib/supabase/api.ts diff --git a/apps/web/src/lib/supabase/client.ts b/apps/web/src/lib/supabase/client.ts new file mode 100644 index 0000000..4495087 --- /dev/null +++ b/apps/web/src/lib/supabase/client.ts @@ -0,0 +1,20 @@ +import { createBrowserClient } from '@supabase/ssr'; + +// Component client + +export default function createClient() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + // 개발 환경에서는 에러를 던지고, 프로덕션에서는 콘솔에 경고만 + if (process.env.NODE_ENV === 'development') { + throw new Error('Missing Supabase environment variables'); + } else { + console.warn('Missing Supabase environment variables'); + return null; + } + } + + return createBrowserClient(supabaseUrl, supabaseAnonKey); +} diff --git a/apps/web/app/lib/supabase/middleware.ts b/apps/web/src/lib/supabase/middleware.ts similarity index 100% rename from apps/web/app/lib/supabase/middleware.ts rename to apps/web/src/lib/supabase/middleware.ts diff --git a/apps/web/app/lib/supabase/server.ts b/apps/web/src/lib/supabase/server.ts similarity index 100% rename from apps/web/app/lib/supabase/server.ts rename to apps/web/src/lib/supabase/server.ts diff --git a/apps/web/src/queries/likeSongQuery.ts b/apps/web/src/queries/likeSongQuery.ts new file mode 100644 index 0000000..3ea4ecd --- /dev/null +++ b/apps/web/src/queries/likeSongQuery.ts @@ -0,0 +1,83 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { + deleteLikeSong, + deleteLikeSongArray, + getLikeSongs, + postLikeSong, +} from '@/lib/api/likeSong'; +import { PersonalSong } from '@/types/song'; + +// 🎵 좋아요 한 곡 리스트 가져오기 +export function useLikeSongQuery() { + return useQuery({ + queryKey: ['likeSong'], + queryFn: async () => { + const response = await getLikeSongs(); + if (!response.success) { + return []; + } + return response.data || []; + }, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 5, + }); +} + +// 🎵 곡 좋아요 추가 +export function usePostLikedSongMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (songId: string) => postLikeSong({ songId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + }, + }); +} + +// 🎵 곡 좋아요 취소 +export function useDeleteLikedSongMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (songId: string) => deleteLikeSong({ songId }), + onMutate: async (songId: string) => { + queryClient.cancelQueries({ queryKey: ['likeSong'] }); + const prev = queryClient.getQueryData(['likeSong']); + queryClient.setQueryData(['likeSong'], (old: PersonalSong[]) => + old.filter(song => song.song_id !== songId), + ); + return { prev }; + }, + onError: (error, songId, context) => { + queryClient.setQueryData(['likeSong'], context?.prev); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + }, + }); +} + +// 🎵 여러 곡 좋아요 취소 +export function useDeleteLikeSongArrayMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (songIds: string[]) => deleteLikeSongArray({ songIds }), + onMutate: async (songIds: string[]) => { + queryClient.cancelQueries({ queryKey: ['likeSong'] }); + const prev = queryClient.getQueryData(['likeSong']); + queryClient.setQueryData(['likeSong'], (old: PersonalSong[]) => + old.filter(song => !songIds.includes(song.song_id)), + ); + return { prev }; + }, + onError: (error, songIds, context) => { + queryClient.setQueryData(['likeSong'], context?.prev); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + }, + }); +} diff --git a/apps/web/src/queries/recentSongQuery.ts b/apps/web/src/queries/recentSongQuery.ts new file mode 100644 index 0000000..fc8be2e --- /dev/null +++ b/apps/web/src/queries/recentSongQuery.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getRecentSong } from '@/lib/api/recentSong'; + +export const useRecentSongsQuery = () => { + return useQuery({ + queryKey: ['recentSong'], + queryFn: async () => { + const response = await getRecentSong(); + if (!response.success) { + return []; + } + return response.data || []; + }, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 5, + }); +}; diff --git a/apps/web/src/queries/searchSongQuery.ts b/apps/web/src/queries/searchSongQuery.ts new file mode 100644 index 0000000..8f44e63 --- /dev/null +++ b/apps/web/src/queries/searchSongQuery.ts @@ -0,0 +1,114 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { deleteLikeSong, postLikeSong } from '@/lib/api/likeSong'; +import { getSearchSong } from '@/lib/api/searchSong'; +import { deleteToSingSong, postToSingSong } from '@/lib/api/tosing'; +import { Method } from '@/types/common'; +import { SearchSong } from '@/types/song'; + +export const useSearchSongSongQuery = (search: string, searchType: string) => { + return useQuery({ + queryKey: ['searchSong', search, searchType], + queryFn: async () => { + const response = await getSearchSong(search, searchType); + if (!response.success) { + return []; + } + return response.data || []; + }, + enabled: !!search, + // DB의 값은 고정된 값이므로 캐시를 유지한다 + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 5, + }); +}; + +export const useToggleLikeMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + // 낙관적 업데이트 검증 코드 + // mutationFn: async ({ songId, method }: { songId: string; method: Method }) => { + // await new Promise(resolve => setTimeout(resolve, 2000)); + mutationFn: ({ songId, method }: { songId: string; method: Method }) => { + if (method === 'POST') { + return postLikeSong({ songId }); + } else { + return deleteLikeSong({ songId }); + } + }, + onMutate: async ({ + songId, + method, + query, + searchType, + }: { + songId: string; + method: Method; + query: string; + searchType: string; + }) => { + queryClient.cancelQueries({ queryKey: ['searchSong', query, searchType] }); + const prev = queryClient.getQueryData(['searchSong', query, searchType]); + const isLiked = method === 'POST'; + queryClient.setQueryData(['searchSong', query, searchType], (old: SearchSong[] = []) => + old.map(song => (song.id === songId ? { ...song, isLiked } : song)), + ); + + return { prev, query, searchType }; + }, + onError: (error, variables, context) => { + queryClient.setQueryData(['searchSong', context?.query, context?.searchType], context?.prev); + }, + onSettled: (data, error, context) => { + queryClient.invalidateQueries({ + queryKey: ['searchSong', context?.query, context?.searchType], + }); + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + queryClient.invalidateQueries({ queryKey: ['recentSong'] }); + }, + }); +}; + +export const useToggleToSingMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ songId, method }: { songId: string; method: Method }) => { + if (method === 'POST') { + return postToSingSong({ songId }); + } else { + return deleteToSingSong({ songId }); + } + }, + onMutate: async ({ + songId, + method, + query, + searchType, + }: { + songId: string; + method: Method; + query: string; + searchType: string; + }) => { + queryClient.cancelQueries({ queryKey: ['searchSong', query, searchType] }); + const prev = queryClient.getQueryData(['searchSong', query, searchType]); + const isToSing = method === 'POST'; + queryClient.setQueryData(['searchSong', query, searchType], (old: SearchSong[] = []) => + old.map(song => (song.id === songId ? { ...song, isToSing } : song)), + ); + return { prev, query, searchType }; + }, + onError: (error, variables, context) => { + queryClient.setQueryData(['searchSong', context?.query, context?.searchType], context?.prev); + }, + onSettled: (data, error, context) => { + queryClient.invalidateQueries({ + queryKey: ['searchSong', context?.query, context?.searchType], + }); + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + queryClient.invalidateQueries({ queryKey: ['recentSong'] }); + queryClient.invalidateQueries({ queryKey: ['toSingSong'] }); + }, + }); +}; diff --git a/apps/web/src/queries/singLogQuery.ts b/apps/web/src/queries/singLogQuery.ts new file mode 100644 index 0000000..9d4cc64 --- /dev/null +++ b/apps/web/src/queries/singLogQuery.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { postSingLog } from '@/lib/api/singLog'; +import { postTotalStat } from '@/lib/api/totalStat'; +import { postUserStats } from '@/lib/api/userStat'; + +export const usePostSingLogMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (songId: string) => { + return Promise.all([ + postSingLog(songId), + postTotalStat({ songId, countType: 'sing_count', isMinus: false }), + postUserStats(songId), + ]); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['recentSong'] }); + queryClient.invalidateQueries({ queryKey: ['userStat'] }); + }, + }); +}; diff --git a/apps/web/src/queries/tosingSongQuery.ts b/apps/web/src/queries/tosingSongQuery.ts new file mode 100644 index 0000000..f73a74e --- /dev/null +++ b/apps/web/src/queries/tosingSongQuery.ts @@ -0,0 +1,134 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { + deleteToSingSong, + deleteToSingSongArray, + getToSingSong, + patchToSingSong, + postToSingSongArray, +} from '@/lib/api/tosing'; +import { ToSingSong } from '@/types/song'; + +// 🎵 부를 노래 목록 가져오기 +export function useToSingSongQuery() { + return useQuery({ + queryKey: ['toSingSong'], + queryFn: async () => { + const response = await getToSingSong(); + if (!response.success) { + return []; + } + return response.data || []; + }, + // DB의 값은 고정된 값이므로 캐시를 유지한다 + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 5, + }); +} + +// 🎵 부를 노래 추가 +export function usePostToSingSongMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (songIds: string[]) => postToSingSongArray({ songIds }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['toSingSong'] }); + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + queryClient.invalidateQueries({ queryKey: ['recentSong'] }); + }, + }); +} + +// 🎵 여러 곡 부를 노래 추가 +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: ['recentSong'] }); + }, + }); +} + +// 🎵 부를 노래 삭제 +export function useDeleteToSingSongMutation() { + const queryClient = useQueryClient(); + + 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 }; + }, + onError: (error, variables, context) => { + queryClient.setQueryData(['toSingSong'], context?.prev); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['toSingSong'] }); + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + queryClient.invalidateQueries({ queryKey: ['recentSong'] }); + }, + }); +} + +// 🎵 여러 곡 부를 노래 삭제 +export function useDeleteToSingSongArrayMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (songIds: string[]) => deleteToSingSongArray({ songIds }), + onMutate: async (songIds: string[]) => { + queryClient.cancelQueries({ queryKey: ['toSingSong'] }); + const prev = queryClient.getQueryData(['toSingSong']); + queryClient.setQueryData(['toSingSong'], (old: ToSingSong[]) => + old.filter(song => !songIds.includes(song.songs.id)), + ); + return { prev }; + }, + onError: (error, variables, context) => { + queryClient.setQueryData(['toSingSong'], context?.prev); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['toSingSong'] }); + queryClient.invalidateQueries({ queryKey: ['likeSong'] }); + queryClient.invalidateQueries({ queryKey: ['recentSong'] }); + }, + }); +} + +// 🎵 부를 노래 순서 변경 +export function usePatchToSingSongMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + songId, + newWeight, + }: { + songId: string; + newWeight: number; + newItems: ToSingSong[]; + }) => patchToSingSong({ songId, newWeight }), + onMutate: async ({ newItems }) => { + queryClient.cancelQueries({ queryKey: ['toSingSong'] }); + const prev = queryClient.getQueryData(['toSingSong']); + // newItems으로 전체 쿼리 교체 + queryClient.setQueryData(['toSingSong'], newItems); + return { prev }; + }, + onError: (error, variables, context) => { + queryClient.setQueryData(['toSingSong'], context?.prev); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['toSingSong'] }); + }, + }); +} diff --git a/apps/web/src/queries/userStatQuery.ts b/apps/web/src/queries/userStatQuery.ts new file mode 100644 index 0000000..b0b244e --- /dev/null +++ b/apps/web/src/queries/userStatQuery.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getUserStats } from '@/lib/api/userStat'; + +export const useUserStatQuery = () => { + return useQuery({ + queryKey: ['userStat'], + queryFn: async () => { + const response = await getUserStats(); + if (!response.success) { + return []; + } + return response.data || []; + }, + }); +}; diff --git a/apps/web/src/query.tsx b/apps/web/src/query.tsx new file mode 100644 index 0000000..a2e2998 --- /dev/null +++ b/apps/web/src/query.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useState } from 'react'; + +export default function QueryProvider({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: true, + throwOnError: true, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 5, + }, + }, + }), + ); + + return {children}; +} diff --git a/apps/web/app/stores/middleware.ts b/apps/web/src/stores/middleware.ts similarity index 100% rename from apps/web/app/stores/middleware.ts rename to apps/web/src/stores/middleware.ts diff --git a/apps/web/app/stores/useAuthStore.ts b/apps/web/src/stores/useAuthStore.ts similarity index 86% rename from apps/web/app/stores/useAuthStore.ts rename to apps/web/src/stores/useAuthStore.ts index d91d05b..56709cb 100644 --- a/apps/web/app/stores/useAuthStore.ts +++ b/apps/web/src/stores/useAuthStore.ts @@ -3,14 +3,12 @@ import { toast } from 'sonner'; import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; -import createClient from '@/lib/supabase/client'; import { User } from '@/types/user'; import { getSupabaseErrorMessage } from '@/utils/getErrorMessage'; +import { getSupabase } from '@/utils/getSupabase'; import { withLoading } from './middleware'; -const supabase = createClient(); - // 사용자 타입 정의 interface AuthState { @@ -48,6 +46,7 @@ const useAuthStore = create( register: async (email, password) => { return await withLoading(set, get, async () => { try { + const supabase = getSupabase(); const { data, error } = await supabase.auth.signUp({ email, password }); if (error) throw error; @@ -80,10 +79,16 @@ const useAuthStore = create( login: async (email, password) => { return await withLoading(set, get, async () => { try { + const supabase = getSupabase(); + const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) throw error; toast.success('로그인 성공', { description: '다시 만나서 반가워요!' }); - return { isSuccess: true, title: '로그인 성공', message: '다시 만나서 반가워요!' }; + return { + isSuccess: true, + title: '로그인 성공', + message: '다시 만나서 반가워요!', + }; } catch (error) { const { code } = error as AuthError; return getSupabaseErrorMessage(code as string); @@ -92,6 +97,8 @@ const useAuthStore = create( }, authKaKaoLogin: async () => { try { + const supabase = getSupabase(); + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'kakao', options: { @@ -113,40 +120,40 @@ const useAuthStore = create( }, // 로그아웃 액션 logout: async () => { + const supabase = getSupabase(); + await supabase.auth.signOut(); set({ user: null, isAuthenticated: false }); }, // 인증 상태 확인 checkAuth: async () => { - try { - const { data, error } = await supabase.auth.getUser(); + const supabase = getSupabase(); - if (error) throw error; - if (!get().user) { - const id = data.user.id; - const { data: existingUser } = await supabase - .from('users') - .select('*') - .eq('id', id) - .single(); + const { data, error } = await supabase.auth.getUser(); + if (error) return false; + if (!get().user) { + const id = data.user.id; + const { data: existingUser } = await supabase + .from('users') + .select('*') + .eq('id', id) + .single(); - if (!existingUser) get().insertUser(id); - else { - set(state => { - state.user = existingUser; - state.isAuthenticated = true; - }); - } + if (!existingUser) get().insertUser(id); + else { + set(state => { + state.user = existingUser; + state.isAuthenticated = true; + }); } - return true; - } catch (error) { - console.error('checkAuth 오류:', error); - return false; } + return true; }, insertUser: async (id: string) => { try { + const supabase = getSupabase(); + const { data: user, error } = await supabase.from('users').insert({ id }).select().single(); if (error) throw error; set(state => { @@ -177,6 +184,7 @@ const useAuthStore = create( return false; } + const supabase = getSupabase(); const result = await supabase .from('users') .update({ nickname: nickname }) @@ -202,6 +210,8 @@ const useAuthStore = create( sendPasswordResetLink: async (email: string) => { return await withLoading(set, get, async () => { try { + const supabase = getSupabase(); + const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/update-password`, }); @@ -223,6 +233,8 @@ const useAuthStore = create( changePassword: async (password: string) => { return await withLoading(set, get, async () => { try { + const supabase = getSupabase(); + const { error } = await supabase.auth.updateUser({ password }); if (error) throw error; diff --git a/apps/web/app/stores/useLoadingStore.ts b/apps/web/src/stores/useLoadingStore.ts similarity index 100% rename from apps/web/app/stores/useLoadingStore.ts rename to apps/web/src/stores/useLoadingStore.ts diff --git a/apps/web/app/stores/useModalStore.ts b/apps/web/src/stores/useModalStore.ts similarity index 100% rename from apps/web/app/stores/useModalStore.ts rename to apps/web/src/stores/useModalStore.ts diff --git a/apps/web/src/stores/useSongStore.ts b/apps/web/src/stores/useSongStore.ts new file mode 100644 index 0000000..db25c76 --- /dev/null +++ b/apps/web/src/stores/useSongStore.ts @@ -0,0 +1,78 @@ +// import { create } from 'zustand'; + +// import { deleteLikeSongArray, getLikeSongs } from '@/lib/api/likeSong'; +// import { getRecentSong } from '@/lib/api/recentSong'; +// import { deleteToSingSong, getToSingSong, postToSingSongArray } from '@/lib/api/tosing'; +// import { AddListModalSong, ToSingSong } from '@/types/song'; +// import { isSuccessResponse } from '@/utils/isSuccessResponse'; + +// interface SongStore { +// toSings: ToSingSong[]; +// likedSongs: AddListModalSong[]; +// recentSongs: AddListModalSong[]; +// swapToSings: (toSings: ToSingSong[]) => void; +// refreshToSings: () => Promise; +// refreshLikeSongs: () => Promise; +// refreshRecentSongs: () => Promise; +// postToSingSong: (songIds: string[]) => Promise; +// deleteToSingSong: (songId: string) => Promise; +// deleteLikeSong: (songIds: string[]) => Promise; +// } + +// const useSongStore = create((set, get) => ({ +// toSings: [], +// likedSongs: [], +// recentSongs: [], + +// swapToSings: (toSings: ToSingSong[]) => { +// set({ toSings }); +// }, + +// refreshToSings: async () => { +// const response = await getToSingSong(); +// if (isSuccessResponse(response) && response.data) { +// set({ toSings: response.data }); +// } +// }, + +// refreshLikeSongs: async () => { +// const response = await getLikeSongs(); +// if (isSuccessResponse(response) && response.data) { +// set({ likedSongs: response.data }); +// } +// }, + +// refreshRecentSongs: async () => { +// const response = await getRecentSong(); +// if (isSuccessResponse(response) && response.data) { +// set({ recentSongs: response.data }); +// } +// }, + +// postToSingSong: async (songIds: string[]) => { +// const response = await postToSingSongArray({ songIds }); +// if (isSuccessResponse(response)) { +// get().refreshToSings(); +// get().refreshLikeSongs(); +// get().refreshRecentSongs(); +// } +// }, + +// deleteToSingSong: async (songId: string) => { +// const response = await deleteToSingSong({ songId }); +// if (isSuccessResponse(response)) { +// get().refreshToSings(); +// get().refreshLikeSongs(); +// get().refreshRecentSongs(); +// } +// }, + +// deleteLikeSong: async (songIds: string[]) => { +// const response = await deleteLikeSongArray({ songIds }); +// if (isSuccessResponse(response)) { +// get().refreshLikeSongs(); +// } +// }, +// })); + +// export default useSongStore; diff --git a/apps/web/src/types/apiRoute.ts b/apps/web/src/types/apiRoute.ts new file mode 100644 index 0000000..278803a --- /dev/null +++ b/apps/web/src/types/apiRoute.ts @@ -0,0 +1,12 @@ +export interface ApiSuccessResponse { + success: true; + data?: T; + // data: T; 타입 에러 +} + +export interface ApiErrorResponse { + success: false; + error?: string; +} + +export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; diff --git a/apps/web/app/types/common.ts b/apps/web/src/types/common.ts similarity index 100% rename from apps/web/app/types/common.ts rename to apps/web/src/types/common.ts diff --git a/apps/web/app/types/likeLog.ts b/apps/web/src/types/likeLog.ts similarity index 100% rename from apps/web/app/types/likeLog.ts rename to apps/web/src/types/likeLog.ts diff --git a/apps/web/app/types/singLog.ts b/apps/web/src/types/singLog.ts similarity index 100% rename from apps/web/app/types/singLog.ts rename to apps/web/src/types/singLog.ts diff --git a/apps/web/app/types/song.ts b/apps/web/src/types/song.ts similarity index 61% rename from apps/web/app/types/song.ts rename to apps/web/src/types/song.ts index 51c7a44..ab6cb7d 100644 --- a/apps/web/app/types/song.ts +++ b/apps/web/src/types/song.ts @@ -1,3 +1,8 @@ +export interface ToSingSong { + order_weight: number; + songs: Song; +} + export interface Song { id: string; title: string; @@ -6,16 +11,19 @@ export interface Song { num_ky: string; } +// 좋아요 곡과 최근 곡에서 공통으로 사용하는 타입 +export interface PersonalSong extends Song { + user_id: string; + song_id: string; + created_at: string; + isInToSingList: boolean; +} + export interface SearchSong extends Song { isLiked: boolean; isToSing: boolean; } -export interface ToSing { - order_weight: number; - songs: Song; -} - export interface AddListModalSong extends Song { isInToSingList: boolean; id: string; diff --git a/apps/web/app/types/user.ts b/apps/web/src/types/user.ts similarity index 100% rename from apps/web/app/types/user.ts rename to apps/web/src/types/user.ts diff --git a/apps/web/app/types/userStat.ts b/apps/web/src/types/userStat.ts similarity index 100% rename from apps/web/app/types/userStat.ts rename to apps/web/src/types/userStat.ts diff --git a/apps/web/app/utils/cn.ts b/apps/web/src/utils/cn.ts similarity index 100% rename from apps/web/app/utils/cn.ts rename to apps/web/src/utils/cn.ts diff --git a/apps/web/app/utils/getAuthenticatedUser.ts b/apps/web/src/utils/getAuthenticatedUser.ts similarity index 71% rename from apps/web/app/utils/getAuthenticatedUser.ts rename to apps/web/src/utils/getAuthenticatedUser.ts index dc4a786..be7b3f6 100644 --- a/apps/web/app/utils/getAuthenticatedUser.ts +++ b/apps/web/src/utils/getAuthenticatedUser.ts @@ -3,12 +3,11 @@ import { SupabaseClient } from '@supabase/supabase-js'; export async function getAuthenticatedUser(supabase: SupabaseClient): Promise { const { data: { user }, - error: authError, + error, } = await supabase.auth.getUser(); - if (authError || !user) { - throw new Error('User not authenticated'); + if (error || !user) { + throw new Error('User not authenticated', { cause: 'auth' }); } - return user.id; // userId만 반환 } diff --git a/apps/web/app/utils/getErrorMessage.ts b/apps/web/src/utils/getErrorMessage.ts similarity index 100% rename from apps/web/app/utils/getErrorMessage.ts rename to apps/web/src/utils/getErrorMessage.ts diff --git a/apps/web/src/utils/getSupabase.ts b/apps/web/src/utils/getSupabase.ts new file mode 100644 index 0000000..e3ad9f2 --- /dev/null +++ b/apps/web/src/utils/getSupabase.ts @@ -0,0 +1,7 @@ +import createClient from '@/lib/supabase/client'; + +export function getSupabase() { + const supabase = createClient(); + if (!supabase) throw new Error('Supabase client not initialized'); + return supabase; +} diff --git a/apps/web/src/utils/isSuccessResponse.ts b/apps/web/src/utils/isSuccessResponse.ts new file mode 100644 index 0000000..f0328b3 --- /dev/null +++ b/apps/web/src/utils/isSuccessResponse.ts @@ -0,0 +1,5 @@ +import { ApiResponse, ApiSuccessResponse } from '@/types/apiRoute'; + +export function isSuccessResponse(response: ApiResponse): response is ApiSuccessResponse { + return response.success === true; +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 45c5f4a..0653bbf 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -20,7 +20,7 @@ } ], "paths": { - "@/*": ["./app/*"], + "@/*": ["./src/*"], "react": ["./node_modules/@types/react"] // lucide-react : cannot be used as a JSX component. 이슈 해결 } }, diff --git a/package.json b/package.json index 110d520..9893b43 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "my-turborepo", + "name": "singcode-monorepo", "private": true, "scripts": { "build": "turbo run build", diff --git a/packages/format-config/next.js b/packages/format-config/next.js new file mode 100644 index 0000000..37d6f4c --- /dev/null +++ b/packages/format-config/next.js @@ -0,0 +1,23 @@ +export default { + printWidth: 100, // 한 줄 최대 길이 + tabWidth: 2, // 탭 크기 (스페이스 2칸) + singleQuote: true, // 작은따옴표 사용 + trailingComma: "all", // 여러 줄일 때 항상 쉼표 사용 + arrowParens: "avoid", // 화살표 함수 괄호 생략 (ex: x => x) + bracketSpacing: true, // 중괄호 간격 유지 (ex: { foo: bar }) + jsxSingleQuote: false, // JSX에서 작은따옴표 사용 안 함 + endOfLine: "auto", + importOrder: [ + "", + "^@repo/(.*)$", + "^@/(.*)$", + "^../(.*)$", + "^./(.*)$", + ], + importOrderSeparation: true, + importOrderSortSpecifiers: true, + plugins: [ + "@trivago/prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss", + ], +}; diff --git a/packages/format-config/package.json b/packages/format-config/package.json new file mode 100644 index 0000000..9c35be8 --- /dev/null +++ b/packages/format-config/package.json @@ -0,0 +1,15 @@ +{ + "name": "@repo/format-config", + "version": "1.0.0", + "type": "module", + "private": true, + "exports": { + "./next-js": "./next.js" + }, + "description": "", + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "prettier": "^3.5.3", + "prettier-plugin-tailwindcss": "^0.6.11" + } +} diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 228f28c..9a0d4ed 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -1,8 +1,3 @@ -export { default as useSong } from './useSong'; -export { default as useSinger } from './useSinger'; -export { default as useComposer } from './useComposer'; -export { default as useLyricist } from './useLyricist'; -export { default as useNo } from './useNo'; -export { default as useRelease } from './useRelease'; -export { default as usePopular } from './usePopular'; -export type { UseQueryReturn } from './types'; +export * from './open-api'; + +export type { OpenAPIResponse } from './types'; diff --git a/packages/query/src/open-api/index.ts b/packages/query/src/open-api/index.ts new file mode 100644 index 0000000..637af92 --- /dev/null +++ b/packages/query/src/open-api/index.ts @@ -0,0 +1,7 @@ +export { default as useSong } from './useSong'; +export { default as useSinger } from './useSinger'; +export { default as useComposer } from './useComposer'; +export { default as useLyricist } from './useLyricist'; +export { default as useNo } from './useNo'; +export { default as useRelease } from './useRelease'; +export { default as usePopular } from './usePopular'; diff --git a/packages/query/src/useComposer.ts b/packages/query/src/open-api/useComposer.ts similarity index 75% rename from packages/query/src/useComposer.ts rename to packages/query/src/open-api/useComposer.ts index 81327c8..110439e 100644 --- a/packages/query/src/useComposer.ts +++ b/packages/query/src/open-api/useComposer.ts @@ -1,20 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { getComposer, Brand } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetComposerProps { composer: string; brand?: Brand; } -const useComposer = (props: GetComposerProps): UseQueryReturn => { +const useComposer = (props: GetComposerProps): OpenAPIResponse => { const { composer, brand } = props; // queryKey를 위한 brandKey 생성 (없으면 'all' 사용) const brandKey = brand || 'all'; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['composer', composer, brandKey], + queryKey: ['open', 'composer', composer, brandKey], queryFn: () => getComposer({ composer, brand }), }); diff --git a/packages/query/src/useLyricist.ts b/packages/query/src/open-api/useLyricist.ts similarity index 75% rename from packages/query/src/useLyricist.ts rename to packages/query/src/open-api/useLyricist.ts index 1ed52d7..c708905 100644 --- a/packages/query/src/useLyricist.ts +++ b/packages/query/src/open-api/useLyricist.ts @@ -1,20 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { getLyricist, Brand } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetLyricistProps { lyricist: string; brand?: Brand; } -const useLyricist = (props: GetLyricistProps): UseQueryReturn => { +const useLyricist = (props: GetLyricistProps): OpenAPIResponse => { const { lyricist, brand } = props; // queryKey를 위한 brandKey 생성 (없으면 'all' 사용) const brandKey = brand || 'all'; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['lyricist', lyricist, brandKey], + queryKey: ['open', 'lyricist', lyricist, brandKey], queryFn: () => getLyricist({ lyricist, brand }), }); diff --git a/packages/query/src/useNo.ts b/packages/query/src/open-api/useNo.ts similarity index 77% rename from packages/query/src/useNo.ts rename to packages/query/src/open-api/useNo.ts index 3d36862..a39c382 100644 --- a/packages/query/src/useNo.ts +++ b/packages/query/src/open-api/useNo.ts @@ -1,20 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { getNo, Brand } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetNoProps { no: string; brand?: Brand; } -const useNo = (props: GetNoProps): UseQueryReturn => { +const useNo = (props: GetNoProps): OpenAPIResponse => { const { no, brand } = props; // queryKey를 위한 brandKey 생성 (없으면 'all' 사용) const brandKey = brand || 'all'; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['no', no, brandKey], + queryKey: ['open', 'no', no, brandKey], queryFn: () => getNo({ no, brand }), }); diff --git a/packages/query/src/usePopular.ts b/packages/query/src/open-api/usePopular.ts similarity index 74% rename from packages/query/src/usePopular.ts rename to packages/query/src/open-api/usePopular.ts index 2d13679..fe1141c 100644 --- a/packages/query/src/usePopular.ts +++ b/packages/query/src/open-api/usePopular.ts @@ -1,17 +1,17 @@ import { useQuery } from '@tanstack/react-query'; import { getPopular, Brand, Period } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetPopularProps { brand: Brand; period: Period; } -const usePopular = (props: GetPopularProps): UseQueryReturn => { +const usePopular = (props: GetPopularProps): OpenAPIResponse => { const { brand, period } = props; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['popular', brand, period], + queryKey: ['open', 'popular', brand, period], queryFn: () => getPopular({ brand, period }), enabled: Boolean(brand) && Boolean(period), }); diff --git a/packages/query/src/useRelease.ts b/packages/query/src/open-api/useRelease.ts similarity index 75% rename from packages/query/src/useRelease.ts rename to packages/query/src/open-api/useRelease.ts index 047a5a3..21d58e2 100644 --- a/packages/query/src/useRelease.ts +++ b/packages/query/src/open-api/useRelease.ts @@ -1,20 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { getRelease, Brand } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetReleaseProps { release: string; brand?: Brand; } -const useRelease = (props: GetReleaseProps): UseQueryReturn => { +const useRelease = (props: GetReleaseProps): OpenAPIResponse => { const { release, brand } = props; // queryKey를 위한 brandKey 생성 (없으면 'all' 사용) const brandKey = brand || 'all'; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['release', release, brandKey], + queryKey: ['open', 'release', release, brandKey], queryFn: () => getRelease({ release, brand }), }); diff --git a/packages/query/src/useSinger.ts b/packages/query/src/open-api/useSinger.ts similarity index 76% rename from packages/query/src/useSinger.ts rename to packages/query/src/open-api/useSinger.ts index 807af07..d0c00eb 100644 --- a/packages/query/src/useSinger.ts +++ b/packages/query/src/open-api/useSinger.ts @@ -1,20 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { getSinger, Brand } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetSingerProps { singer: string; brand?: Brand; } -const useSinger = (props: GetSingerProps): UseQueryReturn => { +const useSinger = (props: GetSingerProps): OpenAPIResponse => { const { singer, brand } = props; // queryKey를 위한 brandKey 생성 (없으면 'all' 사용) const brandKey = brand || 'all'; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['singer', singer, brandKey], + queryKey: ['open', 'singer', singer, brandKey], queryFn: () => getSinger({ singer, brand }), }); diff --git a/packages/query/src/useSong.ts b/packages/query/src/open-api/useSong.ts similarity index 77% rename from packages/query/src/useSong.ts rename to packages/query/src/open-api/useSong.ts index d5251c3..98c8881 100644 --- a/packages/query/src/useSong.ts +++ b/packages/query/src/open-api/useSong.ts @@ -1,20 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import { getSong, Brand, ResponseType } from '@repo/open-api'; -import { UseQueryReturn } from './types'; +import { OpenAPIResponse } from '../types'; interface GetSongProps { title: string; brand?: Brand; } -const useSong = (props: GetSongProps): UseQueryReturn => { +const useSong = (props: GetSongProps): OpenAPIResponse => { const { title, brand } = props; // queryKey를 위한 brandKey 생성 (없으면 'all' 사용) const brandKey = brand || 'all'; const { data, isLoading, isError, error } = useQuery({ - queryKey: ['song', title, brandKey], + queryKey: ['open', 'song', title, brandKey], queryFn: () => getSong({ title, brand }), }); diff --git a/packages/query/src/types.ts b/packages/query/src/types.ts index a71b4a6..9193aae 100644 --- a/packages/query/src/types.ts +++ b/packages/query/src/types.ts @@ -1,6 +1,6 @@ import { ResponseType } from '@repo/open-api'; -export interface UseQueryReturn { +export interface OpenAPIResponse { data: ResponseType[] | null | undefined; isLoading: boolean; isError: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa91e17..433ece9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,15 +227,15 @@ importers: specifier: ^5.0.3 version: 5.0.3(@types/react@19.0.10)(immer@10.1.1)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)) devDependencies: - '@eslint/eslintrc': - specifier: ^3 - version: 3.3.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/format-config': + specifier: workspace:* + version: link:../../packages/format-config '@tailwindcss/postcss': specifier: ^4.0.15 version: 4.0.15 - '@trivago/prettier-plugin-sort-imports': - specifier: ^5.2.2 - version: 5.2.2(prettier@3.5.3) '@types/node': specifier: ^20 version: 20.17.24 @@ -248,21 +248,9 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) - eslint: - specifier: ^9 - version: 9.22.0(jiti@2.4.2) - eslint-config-next: - specifier: 15.2.2 - version: 15.2.2(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) postcss: specifier: ^8.5.3 version: 8.5.3 - prettier: - specifier: ^3.5.3 - version: 3.5.3 - prettier-plugin-tailwindcss: - specifier: ^0.6.11 - version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3))(prettier@3.5.3) tailwindcss: specifier: ^4.0.15 version: 4.0.15 @@ -324,6 +312,18 @@ importers: specifier: ^8.26.0 version: 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + packages/format-config: + devDependencies: + '@trivago/prettier-plugin-sort-imports': + specifier: ^5.2.2 + version: 5.2.2(prettier@3.5.3) + prettier: + specifier: ^3.5.3 + version: 3.5.3 + prettier-plugin-tailwindcss: + specifier: ^0.6.11 + version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3))(prettier@3.5.3) + packages/open-api: dependencies: axios: @@ -1695,9 +1695,6 @@ packages: '@next/eslint-plugin-next@15.2.1': resolution: {integrity: sha512-6ppeToFd02z38SllzWxayLxjjNfzvc7Wm07gQOKSLjyASvKcXjNStZrLXMHuaWkhjqxe+cnhb2uzfWXm1VEj/Q==} - '@next/eslint-plugin-next@15.2.2': - resolution: {integrity: sha512-1+BzokFuFQIfLaRxUKf2u5In4xhPV7tUgKcK53ywvFl6+LXHWHpFkcV7VNeKlyQKUotwiq4fy/aDNF9EiUp4RQ==} - '@next/swc-darwin-arm64@15.2.2': resolution: {integrity: sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==} engines: {node: '>= 10'} @@ -1758,10 +1755,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - '@npmcli/fs@3.1.1': resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2435,12 +2428,6 @@ packages: cpu: [x64] os: [win32] - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@rushstack/eslint-patch@1.11.0': - resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} - '@segment/loosely-validate-event@2.0.0': resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} @@ -2671,9 +2658,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -3022,10 +3006,6 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -3042,10 +3022,6 @@ packages: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} - engines: {node: '>= 0.4'} - array.prototype.flat@1.3.3: resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} engines: {node: '>= 0.4'} @@ -3065,9 +3041,6 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -3101,17 +3074,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.10.3: - resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} - engines: {node: '>=4'} - axios@1.8.3: resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==} - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - babel-core@7.0.0-bridge.0: resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -3573,9 +3538,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -3876,74 +3838,12 @@ packages: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} - eslint-config-next@15.2.2: - resolution: {integrity: sha512-g34RI7RFS4HybYFwGa/okj+8WZM+/fy+pEM+aqRQoVvM4gQhKrd4wIEddKmlZfWD75j8LTwB5zwkmNv3DceH1A==} - peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - eslint-config-prettier@10.1.1: resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@3.8.5: - resolution: {integrity: sha512-0ZRnzOqKc7TRm85w6REOUkVLHevN6nWd/xZsmKhSD/dcDktoxQaQAg59e5EK/QEsGFf7o5JSpE6qTwCEz0WjTw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - - eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.31.0: - resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-jsx-a11y@6.10.2: - resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-only-warn@1.1.0: resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} engines: {node: '>=6'} @@ -4402,9 +4302,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} - get-uri@6.0.4: resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==} engines: {node: '>= 14'} @@ -4675,9 +4572,6 @@ packages: is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - is-bun-module@1.3.0: - resolution: {integrity: sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==} - is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -5098,10 +4992,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -5128,13 +5018,6 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -5685,10 +5568,6 @@ packages: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -6330,9 +6209,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve-workspace-root@2.0.0: resolution: {integrity: sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw==} @@ -6637,9 +6513,6 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - stable-hash@0.0.4: - resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} - stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} @@ -6696,10 +6569,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} - string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -6734,10 +6603,6 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -6956,9 +6821,6 @@ packages: '@swc/wasm': optional: true - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -9169,10 +9031,6 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/eslint-plugin-next@15.2.2': - dependencies: - fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.2.2': optional: true @@ -9209,8 +9067,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nolyfill/is-core-module@1.0.39': {} - '@npmcli/fs@3.1.1': dependencies: semver: 7.7.1 @@ -9887,10 +9743,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.35.0': optional: true - '@rtsao/scc@1.1.0': {} - - '@rushstack/eslint-patch@1.11.0': {} - '@segment/loosely-validate-event@2.0.0': dependencies: component-type: 1.2.2 @@ -10166,8 +10018,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - '@types/minimatch@5.1.2': {} '@types/node-forge@1.3.11': @@ -10525,8 +10375,6 @@ snapshots: dependencies: tslib: 2.8.1 - aria-query@5.3.2: {} - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -10552,15 +10400,6 @@ snapshots: es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 - array.prototype.findlastindex@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - array.prototype.flat@1.3.3: dependencies: call-bind: 1.0.8 @@ -10595,8 +10434,6 @@ snapshots: asap@2.0.6: {} - ast-types-flow@0.0.8: {} - ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -10627,8 +10464,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.10.3: {} - axios@1.8.3: dependencies: follow-redirects: 1.15.9 @@ -10637,8 +10472,6 @@ snapshots: transitivePeerDependencies: - debug - axobject-query@4.1.0: {} - babel-core@7.0.0-bridge.0(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -11200,8 +11033,6 @@ snapshots: csstype@3.1.3: {} - damerau-levenshtein@1.0.8: {} - data-uri-to-buffer@6.0.2: {} data-urls@3.0.2: @@ -11559,112 +11390,10 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@15.2.2(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2): - dependencies: - '@next/eslint-plugin-next': 15.2.2 - '@rushstack/eslint-patch': 1.11.0 - '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.4(eslint@9.22.0(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.22.0(jiti@2.4.2)) - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-prettier@10.1.1(eslint@9.22.0(jiti@2.4.2)): dependencies: eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 - enhanced-resolve: 5.18.1 - eslint: 9.22.0(jiti@2.4.2) - get-tsconfig: 4.10.0 - is-bun-module: 1.3.0 - stable-hash: 0.0.4 - tinyglobby: 0.2.12 - optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0(jiti@2.4.2)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0(jiti@2.4.2)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jsx-a11y@6.10.2(eslint@9.22.0(jiti@2.4.2)): - dependencies: - aria-query: 5.3.2 - array-includes: 3.1.8 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.10.3 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 9.22.0(jiti@2.4.2) - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 - eslint-plugin-only-warn@1.1.0: {} eslint-plugin-react-hooks@5.2.0(eslint@9.22.0(jiti@2.4.2)): @@ -12238,10 +11967,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.10.0: - dependencies: - resolve-pkg-maps: 1.0.0 - get-uri@6.0.4: dependencies: basic-ftp: 5.0.5 @@ -12572,10 +12297,6 @@ snapshots: is-buffer@1.1.6: {} - is-bun-module@1.3.0: - dependencies: - semver: 7.7.1 - is-callable@1.2.7: {} is-core-module@2.16.1: @@ -13241,10 +12962,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json5@1.0.2: - dependencies: - minimist: 1.2.8 - json5@2.2.3: {} jsonfile@4.0.0: @@ -13272,12 +12989,6 @@ snapshots: kleur@3.0.3: {} - language-subtag-registry@0.3.23: {} - - language-tags@1.0.9: - dependencies: - language-subtag-registry: 0.3.23 - leven@3.1.0: {} levn@0.4.1: @@ -13868,12 +13579,6 @@ snapshots: es-abstract: 1.23.9 es-object-atoms: 1.1.1 - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - object.values@1.2.1: dependencies: call-bind: 1.0.8 @@ -14565,8 +14270,6 @@ snapshots: resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve-workspace-root@2.0.0: {} resolve.exports@2.0.3: {} @@ -14950,8 +14653,6 @@ snapshots: dependencies: minipass: 7.1.2 - stable-hash@0.0.4: {} - stack-generator@2.0.10: dependencies: stackframe: 1.3.4 @@ -15009,12 +14710,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 - string.prototype.includes@2.0.1: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -15075,8 +14770,6 @@ snapshots: dependencies: ansi-regex: 6.1.0 - strip-bom@3.0.0: {} - strip-bom@4.0.0: {} strip-eof@1.0.0: {} @@ -15296,13 +14989,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - tslib@1.14.1: {} tslib@2.8.1: {} diff --git a/turbo.json b/turbo.json index 2107653..22f293d 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,16 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", + "globalEnv": [ + "NEXT_PUBLIC_SUPABASE_URL", + "NEXT_PUBLIC_SUPABASE_ANON_KEY", + "SUPABASE_URL", + "SUPABASE_ANON_KEY", + "KAKAO_REST_API_KEY", + "KAKAO_SECRET_KEY", + "KAKAO_REDIRECT_URI", + "NODE_ENV" + ], "tasks": { "build": { "dependsOn": ["^build"],