From 79e779cce78857879d760a35995f405a68e12b5c Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 9 Apr 2025 17:14:17 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat=20:=20tosings=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=88=9C=EC=84=9C=20=EC=9D=B4=EB=8F=99,?= =?UTF-8?q?=20=EB=82=99=EA=B4=80=EC=A0=81=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/api/search/route.ts | 3 +- apps/web/app/api/songs/like/route.ts | 16 +- apps/web/app/api/songs/tosing/route.ts | 106 ++++++++++-- apps/web/app/home/SongCard.tsx | 29 ++-- apps/web/app/home/SongList.tsx | 184 ++++++++++++++------- apps/web/app/home/page.tsx | 2 +- apps/web/app/search/SearchResultCard.tsx | 5 +- apps/web/app/search/page.tsx | 31 ++-- apps/web/app/types/song.ts | 6 + apps/web/app/utils/getAuthenticatedUser.ts | 14 ++ 10 files changed, 285 insertions(+), 111 deletions(-) create mode 100644 apps/web/app/utils/getAuthenticatedUser.ts diff --git a/apps/web/app/api/search/route.ts b/apps/web/app/api/search/route.ts index 4c47836..cae98f8 100644 --- a/apps/web/app/api/search/route.ts +++ b/apps/web/app/api/search/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { SearchSong } from '@/types/song'; +import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; interface ApiResponse { success: boolean; @@ -15,7 +16,6 @@ export async function GET(request: Request): Promise {/* 메인 콘텐츠 영역 */} -
+
{/* 노래 정보 */} -
+
{/* 제목 및 가수 */}
-

{song.title}

-

{song.artist}

+

{title}

+

{artist}

{/* 노래방 번호 */} -
-
+
+
TJ - {song.tjNumber} + {num_tj}
-
+
금영 - {song.kumyoungNumber} + {num_ky}
+ {/* 버튼 영역 - 우측 하단에 고정 */}
+ + + + + ); +} diff --git a/apps/web/app/home/ModalSongItem.tsx b/apps/web/app/home/ModalSongItem.tsx new file mode 100644 index 0000000..aa394ea --- /dev/null +++ b/apps/web/app/home/ModalSongItem.tsx @@ -0,0 +1,34 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { AddListModalSong } from '@/types/song'; +import { cn } from '@/utils/cn'; + +// 노래 항목 컴포넌트 +export default function ModalSongItem({ + song, + isSelected, + onToggleSelect, +}: { + song: AddListModalSong; + isSelected: boolean; + onToggleSelect: (id: string) => void; +}) { + return ( +
+ onToggleSelect(song.id)} + disabled={song.isInToSingList} + /> +
+

{song.title}

+

{song.artist}

+
+
+ ); +} diff --git a/apps/web/app/home/SongList.tsx b/apps/web/app/home/SongList.tsx index f6eb2c4..20b822f 100644 --- a/apps/web/app/home/SongList.tsx +++ b/apps/web/app/home/SongList.tsx @@ -2,7 +2,6 @@ import { DndContext, - DragEndEvent, KeyboardSensor, PointerSensor, closestCenter, @@ -12,21 +11,18 @@ import { import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { SortableContext, - arrayMove, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; -import { useEffect, useState } from 'react'; +import useToSingList from '@/hooks/useToSingList'; import { ToSing } from '@/types/song'; import SongCard from './SongCard'; -// import dynamic from 'next/dynamic'; -// const SongCard = dynamic(() => import('./SongCard'), { ssr: false }); - export default function SongList() { - const [toSings, setToSings] = useState([]); + const { toSings, handleDragEnd, handleDelete, handleMoveToTop, handleMoveToBottom, handleSung } = + useToSingList(); const sensors = useSensors( useSensor(PointerSensor), @@ -35,160 +31,6 @@ export default function SongList() { }), ); - const handleDragEnd = async (event: DragEndEvent) => { - 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 fetch(`/api/songs/tosing`, { - method: 'PATCH', - body: JSON.stringify({ - songId: active.id, - newWeight, - }), - headers: { 'Content-Type': 'application/json' }, - }); - const { success } = await response.json(); - - if (success) { - setToSings(newItems); - } else { - handleSearch(); - } - }; - - const handleDelete = async (songId: string) => { - const response = await fetch(`/api/songs/tosing`, { - method: 'DELETE', - body: JSON.stringify({ songId }), - headers: { 'Content-Type': 'application/json' }, - }); - - const newItem = toSings.filter(item => item.songs.id !== songId); - const { success } = await response.json(); - if (success) { - setToSings(newItem); - } else { - handleSearch(); - } - }; - - const handleMoveToTop = async (songId: string, oldIndex: number) => { - if (oldIndex === 0) return; - const newItems = arrayMove(toSings, oldIndex, 0); - - const newWeight = toSings[0].order_weight - 1; - - const response = await fetch(`/api/songs/tosing`, { - method: 'PATCH', - body: JSON.stringify({ - songId, - newWeight, - }), - headers: { 'Content-Type': 'application/json' }, - }); - const { success } = await response.json(); - - if (success) { - setToSings(newItems); - } else { - handleSearch(); - } - }; - - const handleMoveToBottom = async (songId: string, oldIndex: number) => { - const lastIndex = toSings.length - 1; - if (oldIndex === lastIndex) return; - - const newItems = arrayMove(toSings, oldIndex, lastIndex); - const newWeight = toSings[lastIndex].order_weight + 1; - - const response = await fetch(`/api/songs/tosing`, { - method: 'PATCH', - body: JSON.stringify({ - songId, - newWeight, - }), - headers: { 'Content-Type': 'application/json' }, - }); - const { success } = await response.json(); - - if (success) { - setToSings(newItems); - } else { - handleSearch(); - } - }; - - const handleSung = async (songId: string) => { - handleMoveToBottom( - songId, - toSings.findIndex(item => item.songs.id === songId), - ); - - await fetch(`/api/songs/total_stats`, { - method: 'POST', - body: JSON.stringify({ - songId, - countType: 'sing_count', - }), - headers: { 'Content-Type': 'application/json' }, - }); - - await fetch(`/api/songs/user_stats`, { - method: 'POST', - body: JSON.stringify({ - songId, - }), - headers: { 'Content-Type': 'application/json' }, - }); - - await fetch('/api/sing_logs', { - method: 'POST', - body: JSON.stringify({ - songId, - }), - headers: { 'Content-Type': 'application/json' }, - }); - }; - - const handleSearch = async () => { - const response = await fetch(`/api/songs/tosing`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }); - const { data, success } = await response.json(); - if (success) { - setToSings(data); - } - }; - - useEffect(() => { - handleSearch(); - }, []); - return (
- {toSings.map((item, index) => ( + {toSings.map((item: ToSing, index: number) => ( -

노래방 플레이리스트

+
+

노래방 플레이리스트

+ +
+ + + setIsModalOpen(false)} />
); } diff --git a/apps/web/app/hooks/useAddListModal.ts b/apps/web/app/hooks/useAddListModal.ts new file mode 100644 index 0000000..65ff464 --- /dev/null +++ b/apps/web/app/hooks/useAddListModal.ts @@ -0,0 +1,72 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { useLoadingStore } from '@/stores/useLoadingStore'; +import { AddListModalSong } from '@/types/song'; + +export function useAddListModal() { + const [activeTab, setActiveTab] = useState('liked'); + const [likedSongs, setLikedSongs] = useState([]); + const [recentSongs, setRecentSongs] = useState([]); + const [songSelected, setSongSelected] = useState([]); + const { startLoading, stopLoading } = useLoadingStore(); + + 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 () => { + const response = await fetch('/api/songs/like'); + const { data } = await response.json(); + setLikedSongs(data); + }); + }; + + const getRecentSongs = async () => { + await handleApiCall(async () => { + const response = await fetch('/api/songs/recent'); + const { data } = await response.json(); + setRecentSongs(data); + }); + }; + + const handleToggleSelect = (songId: string) => { + setSongSelected(prev => + prev.includes(songId) ? prev.filter(id => id !== songId) : [...prev, songId], + ); + }; + + const handleConfirm = async () => { + // TODO: API 호출 로직 구현 + }; + + const totalSelectedCount = songSelected.length; + + useEffect(() => { + getLikedSongs(); + getRecentSongs(); + }, []); + + return { + activeTab, + setActiveTab, + likedSongs, + recentSongs, + songSelected, + handleToggleSelect, + handleConfirm, + totalSelectedCount, + }; +} diff --git a/apps/web/app/hooks/useSearch.ts b/apps/web/app/hooks/useSearch.ts new file mode 100644 index 0000000..00925c0 --- /dev/null +++ b/apps/web/app/hooks/useSearch.ts @@ -0,0 +1,135 @@ +import { useState } from 'react'; + +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 } = 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 response = await fetch(`api/search?q=${search}&type=${searchType}`); + const data = await response.json(); + + if (data.success) { + setSearchResults(data.songs); + } else { + setSearchResults([]); + } + return data.success; + }, + () => { + setSearchResults([]); + }, + ); + }; + + const handleToggleToSing = async (songId: string, method: Method) => { + await handleApiCall(async () => { + const response = await fetch('/api/songs/tosing', { + method, + body: JSON.stringify({ songId }), + headers: { 'Content-Type': 'application/json' }, + }); + + const { success } = await response.json(); + 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 fetch('/api/songs/total_stats', { + method: 'POST', + body: JSON.stringify({ + songId, + countType: 'like_count', + isMinus: method === 'DELETE', + }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await fetch(`/api/songs/like`, { + method, + body: JSON.stringify({ songId }), + headers: { 'Content-Type': 'application/json' }, + }); + + const { success } = await response.json(); + if (success) { + const newResults = searchResults.map(song => { + if (song.id === songId) { + return { ...song, isLiked: !song.isLiked }; + } + return song; + }); + setSearchResults(newResults); + } else { + handleSearch(); + } + return success; + }, handleSearch); + }; + + const handleOpenPlaylistModal = (song: SearchSong) => { + setSelectedSong(song); + setIsModal(true); + }; + + const handleSavePlaylist = async () => {}; + + return { + search, + setSearch, + searchResults, + searchType, + handleSearchTypeChange, + handleSearch, + handleToggleToSing, + handleToggleLike, + handleOpenPlaylistModal, + isModal, + selectedSong, + handleSavePlaylist, + }; +} diff --git a/apps/web/app/hooks/useToSingList.ts b/apps/web/app/hooks/useToSingList.ts new file mode 100644 index 0000000..b00d336 --- /dev/null +++ b/apps/web/app/hooks/useToSingList.ts @@ -0,0 +1,176 @@ +// hooks/useToSingList.ts +import { DragEndEvent } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; +import { useEffect, useState } from 'react'; + +import { useAuthStore } from '@/stores/useAuthStore'; +import { useLoadingStore } from '@/stores/useLoadingStore'; +import { ToSing } from '@/types/song'; + +export default function useToSingList() { + const [toSings, setToSings] = useState([]); + const { startLoading, stopLoading } = useLoadingStore(); + const { isAuthenticated } = useAuthStore(); + + 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 () => { + const response = await fetch('/api/songs/tosing'); + const { success, data } = await response.json(); + if (success) { + setToSings(data); + } + return success; + }); + }; + + 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 fetch(`/api/songs/tosing`, { + method: 'PATCH', + body: JSON.stringify({ + songId: active.id, + newWeight, + }), + headers: { 'Content-Type': 'application/json' }, + }); + const { success } = await response.json(); + + setToSings(newItems); + return success; + }, handleSearch); + }; + + const handleDelete = async (songId: string) => { + await handleApiCall(async () => { + const response = await fetch('/api/songs/tosing', { + method: 'DELETE', + body: JSON.stringify({ songId }), + headers: { 'Content-Type': 'application/json' }, + }); + const { success } = await response.json(); + setToSings(prev => prev.filter(item => item.songs.id !== songId)); + return success; + }, handleSearch); + }; + + 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 fetch('/api/songs/tosing', { + method: 'PATCH', + body: JSON.stringify({ songId, newWeight }), + headers: { 'Content-Type': 'application/json' }, + }); + const { success } = await response.json(); + setToSings(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 fetch('/api/songs/tosing', { + method: 'PATCH', + body: JSON.stringify({ songId, newWeight }), + headers: { 'Content-Type': 'application/json' }, + }); + const { success } = await response.json(); + setToSings(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([ + fetch('/api/songs/total_stats', { + method: 'POST', + body: JSON.stringify({ songId, countType: 'sing_count' }), + headers: { 'Content-Type': 'application/json' }, + }), + fetch('/api/songs/user_stats', { + method: 'POST', + body: JSON.stringify({ songId }), + headers: { 'Content-Type': 'application/json' }, + }), + fetch('/api/sing_logs', { + method: 'POST', + body: JSON.stringify({ songId }), + headers: { 'Content-Type': 'application/json' }, + }), + ]); + }, handleSearch); + }; + + // 초기 데이터 로드 + useEffect(() => { + if (isAuthenticated) { + handleSearch(); + } + }, [isAuthenticated]); + + return { + toSings, + handleDragEnd, + handleSearch, + handleDelete, + handleMoveToTop, + handleMoveToBottom, + handleSung, + }; +} diff --git a/apps/web/app/search/SearchResultCard.tsx b/apps/web/app/search/SearchResultCard.tsx index 01bc581..80051d8 100644 --- a/apps/web/app/search/SearchResultCard.tsx +++ b/apps/web/app/search/SearchResultCard.tsx @@ -2,18 +2,22 @@ import { Heart, MinusCircle, PlusCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; -import { Method } from '@/types/common'; import { SearchSong } from '@/types/song'; interface IProps { song: SearchSong; - onToggleToSing: (songId: string, method: Method) => void; - onToggleLike: (songId: string, method: Method) => void; + onToggleToSing: () => void; + onToggleLike: () => void; + onClickOpenPlaylistModal: () => void; } -// 검색 결과 카드 컴포넌트 -export default function SearchResultCard({ song, onToggleToSing, onToggleLike }: IProps) { - const { id, title, artist, num_tj, num_ky, isToSing, isLiked } = song; +export default function SearchResultCard({ + song, + onToggleToSing, + onToggleLike, + // onClickOpenPlaylistModal, +}: IProps) { + const { title, artist, num_tj, num_ky, isToSing, isLiked } = song; return ( @@ -48,7 +52,7 @@ export default function SearchResultCard({ song, onToggleToSing, onToggleLike }: size="icon" className={`h-8 w-8 ${isToSing ? 'text-primary bg-primary/10' : ''}`} aria-label={isToSing ? '내 노래 목록에서 제거' : '내 노래 목록에 추가'} - onClick={() => onToggleToSing(id, isToSing ? 'DELETE' : 'POST')} + onClick={onToggleToSing} > {isToSing ? (
@@ -66,10 +70,20 @@ export default function SearchResultCard({ song, onToggleToSing, onToggleLike }: size="icon" className={`h-8 w-8 ${isLiked ? 'text-red-500' : ''}`} aria-label={isLiked ? '좋아요 취소' : '좋아요'} - onClick={() => onToggleLike(id, isLiked ? 'DELETE' : 'POST')} + onClick={onToggleLike} > + {/* + */}
diff --git a/apps/web/app/search/page.tsx b/apps/web/app/search/page.tsx index 0ad8ef7..e501b0f 100644 --- a/apps/web/app/search/page.tsx +++ b/apps/web/app/search/page.tsx @@ -1,23 +1,29 @@ 'use client'; import { Mic, Search } from 'lucide-react'; -import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Method } from '@/types/common'; -import { SearchSong } from '@/types/song'; +import useSearch from '@/hooks/useSearch'; import SearchResultCard from './SearchResultCard'; -type SearchType = 'title' | 'artist'; - export default function SearchPage() { - const [query, setQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [searchType, setSearchType] = useState('title'); + const { + search, + setSearch, + searchResults, + searchType, + handleSearchTypeChange, + handleSearch, + handleToggleToSing, + handleToggleLike, + handleOpenPlaylistModal, + // isModal, + // selectedSong, + // handleSavePlaylist, + } = useSearch(); // 엔터 키 처리 const handleKeyDown = (e: React.KeyboardEvent) => { @@ -26,75 +32,9 @@ export default function SearchPage() { } }; - const handleSearchTypeChange = (value: string) => { - setSearchType(value as SearchType); - // 검색 유형이 변경되면 현재 쿼리로 다시 검색 - }; - - // 검색 기능 - const handleSearch = async () => { - if (!query) { - return; - } - setIsSearching(true); - const response = await fetch(`api/search?q=${query}&type=${searchType}`); - const data = await response.json(); - - if (data.success) { - setSearchResults(data.songs); - } else { - setSearchResults([]); - } - setIsSearching(false); - }; - - const handleToggleToSing = async (songId: string, method: Method) => { - const url = `/api/songs/tosing`; - const response = await fetch(url, { - method, - body: JSON.stringify({ songId }), - headers: { 'Content-Type': 'application/json' }, - }); - - const { success } = await response.json(); - if (success) { - const newResults = searchResults.map(song => { - if (song.id === songId) { - return { ...song, isToSing: !song.isToSing }; - } - return song; - }); - setSearchResults(newResults); - } else { - handleSearch(); - } - }; - - const handleToggleLike = async (songId: string, method: Method) => { - const url = `/api/songs/like`; - const response = await fetch(url, { - method, - body: JSON.stringify({ songId }), - headers: { 'Content-Type': 'application/json' }, - }); - - const { success } = await response.json(); - if (success) { - const newResults = searchResults.map(song => { - if (song.id === songId) { - return { ...song, isLiked: !song.isLiked }; - } - return song; - }); - setSearchResults(newResults); - } else { - handleSearch(); - } - }; - return ( -
-
+
+

노래 검색

setQuery(e.target.value)} + value={search} + onChange={e => setSearch(e.target.value)} onKeyDown={handleKeyDown} />
- +
- {isSearching ? ( -
- 검색 중... -
- ) : searchResults.length > 0 ? ( + {searchResults.length > 0 ? (
{searchResults.map((song, index) => ( + handleToggleToSing(song.id, song.isToSing ? 'DELETE' : 'POST') + } + onToggleLike={() => handleToggleLike(song.id, song.isLiked ? 'DELETE' : 'POST')} + onClickOpenPlaylistModal={() => handleOpenPlaylistModal(song)} /> ))}
@@ -150,6 +87,8 @@ export default function SearchPage() {
)}
+ + {/* {isModal && } */}
); } diff --git a/apps/web/app/types/song.ts b/apps/web/app/types/song.ts index 77a4a63..d3aa1f8 100644 --- a/apps/web/app/types/song.ts +++ b/apps/web/app/types/song.ts @@ -15,3 +15,7 @@ export interface ToSing { order_weight: number; songs: Song; } + +export interface AddListModalSong extends Song { + isInToSingList: boolean; +} diff --git a/apps/web/package.json b/apps/web/package.json index f03251b..51e160e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-checkbox": "^1.1.5", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5b88c6..db6bdc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,6 +139,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.0.0) + '@radix-ui/react-checkbox': + specifier: ^1.1.5 + version: 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1761,6 +1764,9 @@ packages: '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/react-arrow@1.1.2': resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} peerDependencies: @@ -1774,6 +1780,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.1.5': + resolution: {integrity: sha512-B0gYIVxl77KYDR25AY9EGe/G//ef85RVBIxQvK+m5pxAC7XihAc/8leMHhDvjvhDu02SBSb6BuytlWr/G7F3+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -1801,6 +1820,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: @@ -1810,6 +1838,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.6': resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==} peerDependencies: @@ -1954,6 +1991,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.3': + resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.2': resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} peerDependencies: @@ -1967,6 +2017,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.3': + resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.2': resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==} peerDependencies: @@ -2007,6 +2070,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.0': + resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tabs@1.1.3': resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==} peerDependencies: @@ -2029,6 +2101,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.1.0': resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: @@ -2038,6 +2119,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.1.1': + resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} peerDependencies: @@ -2056,6 +2146,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -2074,6 +2182,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} @@ -9020,6 +9137,8 @@ snapshots: '@radix-ui/primitive@1.1.1': {} + '@radix-ui/primitive@1.1.2': {} + '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -9029,6 +9148,22 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-checkbox@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) @@ -9052,12 +9187,24 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-context@1.1.1(@types/react@19.0.10)(react@19.0.0)': dependencies: react: 19.0.0 optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-context@1.1.2(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -9211,6 +9358,16 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-presence@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) @@ -9220,6 +9377,15 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-primitive@2.0.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.2.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -9259,6 +9425,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-slot@1.2.0(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-tabs@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -9281,6 +9454,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.10)(react@19.0.0)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) @@ -9288,6 +9467,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-use-controllable-state@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.10)(react@19.0.0)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) @@ -9301,6 +9487,18 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.10)(react@19.0.0)': dependencies: '@radix-ui/rect': 1.1.0 @@ -9315,6 +9513,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-use-size@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/rect@1.1.0': {} '@react-native/assets-registry@0.76.7': {} From 8e9bccdd8bd1d7f12ab54324384e6dab8f72f0a9 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Thu, 10 Apr 2025 23:36:12 +0900 Subject: [PATCH 4/5] =?UTF-8?q?chore=20:=20=EC=BD=94=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/SideBar.tsx | 8 +++----- apps/web/app/api/auth/callback/route.ts | 2 +- .../[type]/[param]/route.ts | 0 apps/web/app/api/search/route.ts | 2 +- apps/web/app/api/sing_logs/route.ts | 2 +- apps/web/app/api/songs/like/route.ts | 2 +- apps/web/app/api/songs/recent/route.tsx | 2 +- apps/web/app/api/songs/tosing/route.ts | 2 +- apps/web/app/api/songs/total_stats/route.ts | 2 +- apps/web/app/api/songs/user_stats/route.ts | 2 +- apps/web/app/auth.tsx | 8 +++----- apps/web/app/components/LoadingOverlay.tsx | 8 +++----- apps/web/app/components/messageDialog.tsx | 4 ++-- apps/web/app/components/ui/sonner.tsx | 4 ++-- apps/web/app/home/AddListModal.tsx | 2 +- apps/web/app/hooks/useAddListModal.ts | 4 ++-- apps/web/app/hooks/useSearch.ts | 2 +- apps/web/app/hooks/useToSingList.ts | 4 ++-- apps/web/app/layout.tsx | 2 +- apps/web/app/lib/supabase/client.ts | 2 +- apps/web/app/lib/supabase/server.ts | 2 +- apps/web/app/login/KakaoLogin.tsx | 2 +- apps/web/app/login/actions.ts | 2 +- apps/web/app/login/page.tsx | 4 ++-- apps/web/app/query.tsx | 6 ++---- apps/web/app/signup/actions.ts | 2 +- apps/web/app/signup/page.tsx | 4 ++-- apps/web/app/stores/useAuthStore.ts | 14 ++++++-------- apps/web/app/stores/useLoadingStore.ts | 4 +++- apps/web/app/stores/useModalStore.ts | 8 +++++--- apps/web/app/types/user.ts | 5 +++++ apps/web/app/update-password/page.tsx | 6 +++--- 32 files changed, 61 insertions(+), 62 deletions(-) rename apps/web/app/api/{open-songs => open_songs}/[type]/[param]/route.ts (100%) create mode 100644 apps/web/app/types/user.ts diff --git a/apps/web/app/SideBar.tsx b/apps/web/app/SideBar.tsx index d896af1..a9b9e41 100644 --- a/apps/web/app/SideBar.tsx +++ b/apps/web/app/SideBar.tsx @@ -14,11 +14,11 @@ import { SheetTitle, SheetTrigger, } from '@/components/ui/sheet'; -import { useAuthStore } from '@/stores/useAuthStore'; +import useAuthStore from '@/stores/useAuthStore'; import { Input } from './components/ui/input'; -const Sidebar = () => { +export default function Sidebar() { // 목업 인증 상태 const { user, isAuthenticated, logout, changeNickname } = useAuthStore(); const [isOpen, setIsOpen] = useState(false); @@ -142,6 +142,4 @@ const Sidebar = () => { ); -}; - -export default Sidebar; +} diff --git a/apps/web/app/api/auth/callback/route.ts b/apps/web/app/api/auth/callback/route.ts index 3b9d4d2..f7427ea 100644 --- a/apps/web/app/api/auth/callback/route.ts +++ b/apps/web/app/api/auth/callback/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; // The client you created from the Server-Side Auth instructions -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); diff --git a/apps/web/app/api/open-songs/[type]/[param]/route.ts b/apps/web/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/app/api/open_songs/[type]/[param]/route.ts diff --git a/apps/web/app/api/search/route.ts b/apps/web/app/api/search/route.ts index cae98f8..a7aa8d3 100644 --- a/apps/web/app/api/search/route.ts +++ b/apps/web/app/api/search/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; import { SearchSong } from '@/types/song'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; diff --git a/apps/web/app/api/sing_logs/route.ts b/apps/web/app/api/sing_logs/route.ts index 9aeabbe..7243855 100644 --- a/apps/web/app/api/sing_logs/route.ts +++ b/apps/web/app/api/sing_logs/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; export async function POST(request: Request) { diff --git a/apps/web/app/api/songs/like/route.ts b/apps/web/app/api/songs/like/route.ts index f0925ea..27739a6 100644 --- a/apps/web/app/api/songs/like/route.ts +++ b/apps/web/app/api/songs/like/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; export async function GET() { diff --git a/apps/web/app/api/songs/recent/route.tsx b/apps/web/app/api/songs/recent/route.tsx index f80807c..2b4a21b 100644 --- a/apps/web/app/api/songs/recent/route.tsx +++ b/apps/web/app/api/songs/recent/route.tsx @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; export async function GET() { diff --git a/apps/web/app/api/songs/tosing/route.ts b/apps/web/app/api/songs/tosing/route.ts index 9bf2167..6927858 100644 --- a/apps/web/app/api/songs/tosing/route.ts +++ b/apps/web/app/api/songs/tosing/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; export async function GET() { diff --git a/apps/web/app/api/songs/total_stats/route.ts b/apps/web/app/api/songs/total_stats/route.ts index 98f97ee..dbb104a 100644 --- a/apps/web/app/api/songs/total_stats/route.ts +++ b/apps/web/app/api/songs/total_stats/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; // 유효한 카운트 타입 정의 type CountType = 'sing_count' | 'like_count' | 'saved_count'; diff --git a/apps/web/app/api/songs/user_stats/route.ts b/apps/web/app/api/songs/user_stats/route.ts index 681fa23..6fb4cd0 100644 --- a/apps/web/app/api/songs/user_stats/route.ts +++ b/apps/web/app/api/songs/user_stats/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; export async function POST(request: Request) { diff --git a/apps/web/app/auth.tsx b/apps/web/app/auth.tsx index 1d4a928..7d65e87 100644 --- a/apps/web/app/auth.tsx +++ b/apps/web/app/auth.tsx @@ -4,9 +4,9 @@ import { usePathname, useRouter } from 'next/navigation'; import { useEffect } from 'react'; import { toast } from 'sonner'; -import { useAuthStore } from '@/stores/useAuthStore'; +import useAuthStore from '@/stores/useAuthStore'; -const AuthProvider = ({ children }: { children: React.ReactNode }) => { +export default function AuthProvider({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const { checkAuth } = useAuthStore(); @@ -29,6 +29,4 @@ const AuthProvider = ({ children }: { children: React.ReactNode }) => { }, [pathname, router, checkAuth]); return <>{children}; -}; - -export default AuthProvider; +} diff --git a/apps/web/app/components/LoadingOverlay.tsx b/apps/web/app/components/LoadingOverlay.tsx index a85822c..fca19d9 100644 --- a/apps/web/app/components/LoadingOverlay.tsx +++ b/apps/web/app/components/LoadingOverlay.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useLoadingStore } from '@/stores/useLoadingStore'; +import useLoadingStore from '@/stores/useLoadingStore'; -const LoadingOverlay = () => { +export default function LoadingOverlay() { const isLoading = useLoadingStore(state => state.isLoading); if (!isLoading) return null; @@ -12,6 +12,4 @@ const LoadingOverlay = () => {
); -}; - -export default LoadingOverlay; +} diff --git a/apps/web/app/components/messageDialog.tsx b/apps/web/app/components/messageDialog.tsx index 38047eb..76edb10 100644 --- a/apps/web/app/components/messageDialog.tsx +++ b/apps/web/app/components/messageDialog.tsx @@ -11,10 +11,10 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { useModalStore } from '@/stores/useModalStore'; +import useModalStore from '@/stores/useModalStore'; import { cn } from '@/utils/cn'; -export function MessageDialog() { +export default function MessageDialog() { const { isOpen, title, message, variant, buttonText, onButtonClick, closeMessage } = useModalStore(); diff --git a/apps/web/app/components/ui/sonner.tsx b/apps/web/app/components/ui/sonner.tsx index 564a654..90197d1 100644 --- a/apps/web/app/components/ui/sonner.tsx +++ b/apps/web/app/components/ui/sonner.tsx @@ -3,7 +3,7 @@ import { useTheme } from 'next-themes'; import { Toaster as Sonner, ToasterProps } from 'sonner'; -const Toaster = ({ ...props }: ToasterProps) => { +function Toaster({ ...props }: ToasterProps) { const { theme = 'system' } = useTheme(); return ( @@ -20,6 +20,6 @@ const Toaster = ({ ...props }: ToasterProps) => { {...props} /> ); -}; +} export { Toaster }; diff --git a/apps/web/app/home/AddListModal.tsx b/apps/web/app/home/AddListModal.tsx index 4281afe..1709adf 100644 --- a/apps/web/app/home/AddListModal.tsx +++ b/apps/web/app/home/AddListModal.tsx @@ -9,7 +9,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { useAddListModal } from '@/hooks/useAddListModal'; +import useAddListModal from '@/hooks/useAddListModal'; import ModalSongItem from './ModalSongItem'; diff --git a/apps/web/app/hooks/useAddListModal.ts b/apps/web/app/hooks/useAddListModal.ts index 65ff464..150ee1f 100644 --- a/apps/web/app/hooks/useAddListModal.ts +++ b/apps/web/app/hooks/useAddListModal.ts @@ -2,10 +2,10 @@ import { useEffect, useState } from 'react'; -import { useLoadingStore } from '@/stores/useLoadingStore'; +import useLoadingStore from '@/stores/useLoadingStore'; import { AddListModalSong } from '@/types/song'; -export function useAddListModal() { +export default function useAddListModal() { const [activeTab, setActiveTab] = useState('liked'); const [likedSongs, setLikedSongs] = useState([]); const [recentSongs, setRecentSongs] = useState([]); diff --git a/apps/web/app/hooks/useSearch.ts b/apps/web/app/hooks/useSearch.ts index 00925c0..ae697b3 100644 --- a/apps/web/app/hooks/useSearch.ts +++ b/apps/web/app/hooks/useSearch.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { useLoadingStore } from '@/stores/useLoadingStore'; +import useLoadingStore from '@/stores/useLoadingStore'; import { Method } from '@/types/common'; import { SearchSong } from '@/types/song'; diff --git a/apps/web/app/hooks/useToSingList.ts b/apps/web/app/hooks/useToSingList.ts index b00d336..334fbbe 100644 --- a/apps/web/app/hooks/useToSingList.ts +++ b/apps/web/app/hooks/useToSingList.ts @@ -3,8 +3,8 @@ import { DragEndEvent } from '@dnd-kit/core'; import { arrayMove } from '@dnd-kit/sortable'; import { useEffect, useState } from 'react'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { useLoadingStore } from '@/stores/useLoadingStore'; +import useAuthStore from '@/stores/useAuthStore'; +import useLoadingStore from '@/stores/useLoadingStore'; import { ToSing } from '@/types/song'; export default function useToSingList() { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index cab8ef6..e2ca111 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from 'next'; import { Toaster } from 'sonner'; import LoadingOverlay from '@/components/LoadingOverlay'; -import { MessageDialog } from '@/components/messageDialog'; +import MessageDialog from '@/components/messageDialog'; import ErrorWrapper from './ErrorWrapper'; import Footer from './Footer'; diff --git a/apps/web/app/lib/supabase/client.ts b/apps/web/app/lib/supabase/client.ts index 6d0e669..62af38f 100644 --- a/apps/web/app/lib/supabase/client.ts +++ b/apps/web/app/lib/supabase/client.ts @@ -2,7 +2,7 @@ import { createBrowserClient } from '@supabase/ssr'; // Component client -export function createClient() { +export default function createClient() { // CSR에서는 Next_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY 사용 return createBrowserClient( diff --git a/apps/web/app/lib/supabase/server.ts b/apps/web/app/lib/supabase/server.ts index 09736eb..928ccf7 100644 --- a/apps/web/app/lib/supabase/server.ts +++ b/apps/web/app/lib/supabase/server.ts @@ -2,7 +2,7 @@ import { createServerClient } from '@supabase/ssr'; import { cookies } from 'next/headers'; // Server client -export async function createClient() { +export default async function createClient() { const cookieStore = await cookies(); return createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { diff --git a/apps/web/app/login/KakaoLogin.tsx b/apps/web/app/login/KakaoLogin.tsx index 85380f9..645250e 100644 --- a/apps/web/app/login/KakaoLogin.tsx +++ b/apps/web/app/login/KakaoLogin.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; -import { useAuthStore } from '@/stores/useAuthStore'; +import useAuthStore from '@/stores/useAuthStore'; // 클라이언트용 Supabase 클라이언트 diff --git a/apps/web/app/login/actions.ts b/apps/web/app/login/actions.ts index 26a49ac..54dc943 100644 --- a/apps/web/app/login/actions.ts +++ b/apps/web/app/login/actions.ts @@ -2,7 +2,7 @@ import { revalidatePath } from 'next/cache'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; export async function login(email: string, password: string) { const supabase = await createClient(); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 0ba865a..ca0907a 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -10,8 +10,8 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { useModalStore } from '@/stores/useModalStore'; +import useAuthStore from '@/stores/useAuthStore'; +import useModalStore from '@/stores/useModalStore'; import KakaoLogin from './KakaoLogin'; diff --git a/apps/web/app/query.tsx b/apps/web/app/query.tsx index 89663fc..67799b3 100644 --- a/apps/web/app/query.tsx +++ b/apps/web/app/query.tsx @@ -3,10 +3,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useState } from 'react'; -const QueryProvider = ({ children }: { children: React.ReactNode }) => { +export default function QueryProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()); return {children}; -}; - -export default QueryProvider; +} diff --git a/apps/web/app/signup/actions.ts b/apps/web/app/signup/actions.ts index c9be383..de4d1ae 100644 --- a/apps/web/app/signup/actions.ts +++ b/apps/web/app/signup/actions.ts @@ -3,7 +3,7 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { createClient } from '@/lib/supabase/server'; +import createClient from '@/lib/supabase/server'; export async function register(email: string, password: string) { const supabase = await createClient(); diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx index 96617cc..84cb112 100644 --- a/apps/web/app/signup/page.tsx +++ b/apps/web/app/signup/page.tsx @@ -8,8 +8,8 @@ import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { useModalStore } from '@/stores/useModalStore'; +import useAuthStore from '@/stores/useAuthStore'; +import useModalStore from '@/stores/useModalStore'; export default function SignupPage() { const [email, setEmail] = useState(''); diff --git a/apps/web/app/stores/useAuthStore.ts b/apps/web/app/stores/useAuthStore.ts index f72ee3e..2629fd3 100644 --- a/apps/web/app/stores/useAuthStore.ts +++ b/apps/web/app/stores/useAuthStore.ts @@ -3,19 +3,15 @@ import { toast } from 'sonner'; import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; -import { createClient } from '@/lib/supabase/client'; +import createClient from '@/lib/supabase/client'; +import { User } from '@/types/user'; import { getErrorMessage } from '@/utils/getErrorMessage'; import { withLoading } from './middleware'; -export const supabase = createClient(); +const supabase = createClient(); // 사용자 타입 정의 -export interface User { - id: string; - nickname: string; - profile_image: string | null; -} interface AuthState { user: User | null; @@ -43,7 +39,7 @@ interface ModalResponseState { errorMessage?: string; } -export const useAuthStore = create( +const useAuthStore = create( immer((set, get) => ({ user: null, isLoading: false, @@ -238,3 +234,5 @@ export const useAuthStore = create( }, })), ); + +export default useAuthStore; diff --git a/apps/web/app/stores/useLoadingStore.ts b/apps/web/app/stores/useLoadingStore.ts index cd5d85a..4f0847e 100644 --- a/apps/web/app/stores/useLoadingStore.ts +++ b/apps/web/app/stores/useLoadingStore.ts @@ -7,7 +7,7 @@ interface LoadingState { stopLoading: () => void; } -export const useLoadingStore = create((set, get) => ({ +const useLoadingStore = create((set, get) => ({ count: 0, isLoading: false, startLoading: () => { @@ -22,3 +22,5 @@ export const useLoadingStore = create((set, get) => ({ }); }, })); + +export default useLoadingStore; diff --git a/apps/web/app/stores/useModalStore.ts b/apps/web/app/stores/useModalStore.ts index d992084..ce1fb3c 100644 --- a/apps/web/app/stores/useModalStore.ts +++ b/apps/web/app/stores/useModalStore.ts @@ -1,8 +1,8 @@ import { create } from 'zustand'; -export type MessageVariant = 'default' | 'success' | 'error' | 'warning' | 'info'; +type MessageVariant = 'default' | 'success' | 'error' | 'warning' | 'info'; -export interface ModalState { +interface ModalState { isOpen: boolean; title?: string; message: string; @@ -21,7 +21,7 @@ export interface ModalState { closeMessage: () => void; } -export const useModalStore = create(set => ({ +const useModalStore = create(set => ({ isOpen: false, title: undefined, message: '', @@ -45,3 +45,5 @@ export const useModalStore = create(set => ({ set({ isOpen: false }); }, })); + +export default useModalStore; diff --git a/apps/web/app/types/user.ts b/apps/web/app/types/user.ts new file mode 100644 index 0000000..f53345a --- /dev/null +++ b/apps/web/app/types/user.ts @@ -0,0 +1,5 @@ +export interface User { + id: string; + nickname: string; + profile_image: string | null; +} diff --git a/apps/web/app/update-password/page.tsx b/apps/web/app/update-password/page.tsx index b994204..fa29e3c 100644 --- a/apps/web/app/update-password/page.tsx +++ b/apps/web/app/update-password/page.tsx @@ -10,9 +10,9 @@ import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { createClient } from '@/lib/supabase/client'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { useModalStore } from '@/stores/useModalStore'; +import createClient from '@/lib/supabase/client'; +import useAuthStore from '@/stores/useAuthStore'; +import useModalStore from '@/stores/useModalStore'; export default function UpdatePasswordPage() { // 상태 관리 From 880c00a532481aa92a07f9879da0957e74981b35 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 11 Apr 2025 18:28:52 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat=20:=20=EB=A1=9C=EB=94=A9=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81,=20toSing=EC=97=90=EC=84=9C=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=9C=EC=96=B4,=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=BC=88=EB=8C=80=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/SideBar.tsx | 2 - apps/web/app/api/search/route.ts | 2 - apps/web/app/api/sing_logs/route.ts | 3 +- apps/web/app/api/songs/tosing/array/route.ts | 42 +++++++++ apps/web/app/api/songs/tosing/route.ts | 1 - apps/web/app/auth.tsx | 1 - apps/web/app/components/LoadingOverlay.tsx | 11 ++- apps/web/app/components/ui/alert.tsx | 60 +++++++++++++ apps/web/app/components/ui/scroll-area.tsx | 56 ++++++++++++ apps/web/app/error.tsx | 91 ++++++++++++++++---- apps/web/app/home/AddListModal.tsx | 48 +++++++---- apps/web/app/home/SongCard.tsx | 2 +- apps/web/app/home/SongList.tsx | 17 +++- apps/web/app/home/page.tsx | 10 ++- apps/web/app/hooks/useAddListModal.ts | 25 +++--- apps/web/app/hooks/useSearch.ts | 8 +- apps/web/app/hooks/useToSingList.ts | 33 ++++--- apps/web/app/library/Menu.tsx | 35 ++++++++ apps/web/app/library/liked/page.tsx | 35 ++++++++ apps/web/app/library/page.tsx | 45 +++++++++- apps/web/app/library/stats/page.tsx | 35 ++++++++ apps/web/app/not-found.tsx | 44 ++++++++++ apps/web/app/page.tsx | 6 +- apps/web/app/popular/RankingList.tsx | 66 ++++++++++++++ apps/web/app/popular/page.tsx | 65 +++++++++++++- apps/web/app/search/page.tsx | 13 ++- apps/web/app/stores/useLoadingStore.ts | 6 ++ apps/web/app/stores/useSongStore.ts | 62 +++++++++++++ apps/web/package.json | 1 + pnpm-lock.yaml | 53 ++++++++++++ 30 files changed, 776 insertions(+), 102 deletions(-) create mode 100644 apps/web/app/api/songs/tosing/array/route.ts create mode 100644 apps/web/app/components/ui/alert.tsx create mode 100644 apps/web/app/components/ui/scroll-area.tsx create mode 100644 apps/web/app/library/Menu.tsx create mode 100644 apps/web/app/library/liked/page.tsx create mode 100644 apps/web/app/library/stats/page.tsx create mode 100644 apps/web/app/not-found.tsx create mode 100644 apps/web/app/popular/RankingList.tsx create mode 100644 apps/web/app/stores/useSongStore.ts diff --git a/apps/web/app/SideBar.tsx b/apps/web/app/SideBar.tsx index a9b9e41..26219ed 100644 --- a/apps/web/app/SideBar.tsx +++ b/apps/web/app/SideBar.tsx @@ -44,8 +44,6 @@ export default function Sidebar() { }; const handleLogin = () => { - console.log('login'); - console.log('isAuthenticated', isAuthenticated); router.push('/login'); setIsOpen(false); }; diff --git a/apps/web/app/api/search/route.ts b/apps/web/app/api/search/route.ts index a7aa8d3..6571d0b 100644 --- a/apps/web/app/api/search/route.ts +++ b/apps/web/app/api/search/route.ts @@ -45,8 +45,6 @@ export async function GET(request: Request): Promise ({ + user_id: userId, + song_id: songId, + order_weight: newWeight, + })), + ); + + if (error) throw error; + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error in tosings API:', error); + return NextResponse.json( + { success: false, error: 'Failed to post tosings song' }, + { status: 500 }, + ); + } +} diff --git a/apps/web/app/api/songs/tosing/route.ts b/apps/web/app/api/songs/tosing/route.ts index 6927858..8bacbb5 100644 --- a/apps/web/app/api/songs/tosing/route.ts +++ b/apps/web/app/api/songs/tosing/route.ts @@ -96,7 +96,6 @@ export async function PATCH(request: Request) { .from('tosings') .update({ order_weight: newWeight }) .match({ user_id: userId, song_id: songId }); - console.log(userId, songId, newWeight); if (error) throw error; return NextResponse.json({ success: true }); diff --git a/apps/web/app/auth.tsx b/apps/web/app/auth.tsx index 7d65e87..6964120 100644 --- a/apps/web/app/auth.tsx +++ b/apps/web/app/auth.tsx @@ -16,7 +16,6 @@ export default function AuthProvider({ children }: { children: React.ReactNode } const allowPaths = ['/login', '/signup', 'update-password']; const isAuthenticated = await checkAuth(); - console.log(pathname); if (!isAuthenticated && !allowPaths.includes(pathname)) { toast.error('로그인이 필요해요.', { description: '로그인 후 이용해주세요.', diff --git a/apps/web/app/components/LoadingOverlay.tsx b/apps/web/app/components/LoadingOverlay.tsx index fca19d9..4aa4639 100644 --- a/apps/web/app/components/LoadingOverlay.tsx +++ b/apps/web/app/components/LoadingOverlay.tsx @@ -1,9 +1,18 @@ 'use client'; +import { useEffect, useState } from 'react'; + import useLoadingStore from '@/stores/useLoadingStore'; export default function LoadingOverlay() { - const isLoading = useLoadingStore(state => state.isLoading); + const { isLoading } = useLoadingStore(); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) return null; if (!isLoading) return null; diff --git a/apps/web/app/components/ui/alert.tsx b/apps/web/app/components/ui/alert.tsx new file mode 100644 index 0000000..4ef8379 --- /dev/null +++ b/apps/web/app/components/ui/alert.tsx @@ -0,0 +1,60 @@ +import { type VariantProps, cva } from 'class-variance-authority'; +import * as React from 'react'; + +import { cn } from '@/utils/cn'; + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription }; diff --git a/apps/web/app/components/ui/scroll-area.tsx b/apps/web/app/components/ui/scroll-area.tsx new file mode 100644 index 0000000..b1731ae --- /dev/null +++ b/apps/web/app/components/ui/scroll-area.tsx @@ -0,0 +1,56 @@ +'use client'; + +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; +import * as React from 'react'; + +import { cn } from '@/utils/cn'; + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ); +} + +function ScrollBar({ + className, + orientation = 'vertical', + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { ScrollArea, ScrollBar }; diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx index 7740aab..e1403e6 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/app/error.tsx @@ -1,5 +1,19 @@ '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'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + type ErrorPageProps = { error: Error; reset: () => void; @@ -11,37 +25,76 @@ interface AuthError { type: string; } -export default function Error({ error, reset }: ErrorPageProps) { +export default function Error({ error }: ErrorPageProps) { const errorMessage = error.message; let errorDetails: AuthError | null = null; + let isAuthError = false; // 에러 메시지 파싱 시도 try { errorDetails = JSON.parse(error.message) as AuthError; + isAuthError = Boolean(errorDetails?.code); } catch { // 파싱 실패 시 기본 메시지 사용 } + // 에러 로깅 + useEffect(() => { + console.error('페이지 에러:', error); + }, [error]); + return ( -
-

인증 오류

- - {errorDetails ? ( -
-

에러 코드: {errorDetails.code}

-

{decodeURIComponent(errorDetails.message)}

- {errorDetails.type === 'access_denied' && ( -

인증 링크가 만료되었거나 유효하지 않습니다.

+
+ + + + {isAuthError ? '인증 오류' : '오류가 발생했습니다'} + + + {isAuthError + ? '인증 과정에서 문제가 발생했습니다' + : '페이지를 로드하는 중 문제가 발생했습니다'} + + + + + + + + {errorDetails?.code ? `에러 코드: ${errorDetails.code}` : '오류 발생'} + + + {errorDetails + ? decodeURIComponent(errorDetails.message) + : errorMessage || '서버에서 오류가 발생했습니다.'} + + + + {errorDetails?.type === 'access_denied' && ( +
+

+ 인증 링크가 만료되었거나 유효하지 않습니다. 새로운 인증 링크를 요청하시거나 다시 + 시도해주세요. +

+
+ )} + + {!isAuthError && ( +
+

+ 일시적인 문제일 수 있습니다. 다시 시도하거나 홈으로 돌아가 다른 기능을 이용해보세요. +

+
)} -
- ) : ( -

{errorMessage || '서버에서 오류가 발생했습니다.'}

- )} - -
- - -
+ + + + + +
); } diff --git a/apps/web/app/home/AddListModal.tsx b/apps/web/app/home/AddListModal.tsx index 1709adf..e5ef39d 100644 --- a/apps/web/app/home/AddListModal.tsx +++ b/apps/web/app/home/AddListModal.tsx @@ -10,6 +10,7 @@ import { } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import useAddListModal from '@/hooks/useAddListModal'; +import useSongStore from '@/stores/useSongStore'; import ModalSongItem from './ModalSongItem'; @@ -22,14 +23,19 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) { const { activeTab, setActiveTab, - likedSongs, - recentSongs, songSelected, handleToggleSelect, handleConfirm, totalSelectedCount, } = useAddListModal(); + const { likedSongs, recentSongs } = useSongStore(); + + const handleClickConfirm = () => { + handleConfirm(); + onClose(); + }; + return ( !open && onClose()}> @@ -53,27 +59,29 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) {
- {likedSongs.map(song => ( - - ))} + {likedSongs && + likedSongs.map(song => ( + + ))}
- {recentSongs.map(song => ( - - ))} + {recentSongs && + recentSongs.map(song => ( + + ))}
@@ -84,7 +92,9 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) { - +
diff --git a/apps/web/app/home/SongCard.tsx b/apps/web/app/home/SongCard.tsx index 1987128..51c8609 100644 --- a/apps/web/app/home/SongCard.tsx +++ b/apps/web/app/home/SongCard.tsx @@ -37,7 +37,7 @@ export default function SongCard({ {/* 노래 정보 */}
{/* 제목 및 가수 */} -
+

{title}

{artist}

diff --git a/apps/web/app/home/SongList.tsx b/apps/web/app/home/SongList.tsx index 20b822f..bc78ef5 100644 --- a/apps/web/app/home/SongList.tsx +++ b/apps/web/app/home/SongList.tsx @@ -14,15 +14,20 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; +import { Loader2 } from 'lucide-react'; import useToSingList from '@/hooks/useToSingList'; +import useLoadingStore from '@/stores/useLoadingStore'; +import useSongStore from '@/stores/useSongStore'; import { ToSing } from '@/types/song'; import SongCard from './SongCard'; export default function SongList() { - const { toSings, handleDragEnd, handleDelete, handleMoveToTop, handleMoveToBottom, handleSung } = + const { handleDragEnd, handleDelete, handleMoveToTop, handleMoveToBottom, handleSung } = useToSingList(); + const { toSings } = useSongStore(); + const { isInitialLoading } = useLoadingStore(); const sensors = useSensors( useSensor(PointerSensor), @@ -43,6 +48,16 @@ export default function SongList() { strategy={verticalListSortingStrategy} >
+ {isInitialLoading && ( +
+ +
+ )} + {!isInitialLoading && toSings.length === 0 && ( +
+

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

+
+ )} {toSings.map((item: ToSing, index: number) => ( +

노래방 플레이리스트

- - + + + setIsModalOpen(false)} />
diff --git a/apps/web/app/hooks/useAddListModal.ts b/apps/web/app/hooks/useAddListModal.ts index 150ee1f..8aa4cac 100644 --- a/apps/web/app/hooks/useAddListModal.ts +++ b/apps/web/app/hooks/useAddListModal.ts @@ -3,14 +3,15 @@ import { useEffect, useState } from 'react'; import useLoadingStore from '@/stores/useLoadingStore'; -import { AddListModalSong } from '@/types/song'; +import useSongStore from '@/stores/useSongStore'; export default function useAddListModal() { const [activeTab, setActiveTab] = useState('liked'); - const [likedSongs, setLikedSongs] = useState([]); - const [recentSongs, setRecentSongs] = useState([]); + const [songSelected, setSongSelected] = useState([]); - const { startLoading, stopLoading } = useLoadingStore(); + const { startLoading, stopLoading, initialLoading } = useLoadingStore(); + + const { refreshLikedSongs, refreshRecentSongs, postToSingSongs } = useSongStore(); const handleApiCall = async (apiCall: () => Promise, onError?: () => void) => { startLoading(); @@ -28,17 +29,13 @@ export default function useAddListModal() { const getLikedSongs = async () => { await handleApiCall(async () => { - const response = await fetch('/api/songs/like'); - const { data } = await response.json(); - setLikedSongs(data); + refreshLikedSongs(); }); }; const getRecentSongs = async () => { await handleApiCall(async () => { - const response = await fetch('/api/songs/recent'); - const { data } = await response.json(); - setRecentSongs(data); + refreshRecentSongs(); }); }; @@ -49,7 +46,10 @@ export default function useAddListModal() { }; const handleConfirm = async () => { - // TODO: API 호출 로직 구현 + await handleApiCall(async () => { + await postToSingSongs(songSelected); + setSongSelected([]); + }); }; const totalSelectedCount = songSelected.length; @@ -57,13 +57,12 @@ export default function useAddListModal() { useEffect(() => { getLikedSongs(); getRecentSongs(); + initialLoading(); }, []); return { activeTab, setActiveTab, - likedSongs, - recentSongs, songSelected, handleToggleSelect, handleConfirm, diff --git a/apps/web/app/hooks/useSearch.ts b/apps/web/app/hooks/useSearch.ts index ae697b3..fba4cc9 100644 --- a/apps/web/app/hooks/useSearch.ts +++ b/apps/web/app/hooks/useSearch.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import useLoadingStore from '@/stores/useLoadingStore'; import { Method } from '@/types/common'; @@ -10,7 +10,7 @@ export default function useSearch() { const [search, setSearch] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searchType, setSearchType] = useState('title'); - const { startLoading, stopLoading } = useLoadingStore(); + const { startLoading, stopLoading, initialLoading } = useLoadingStore(); const [isModal, setIsModal] = useState(false); const [selectedSong, setSelectedSong] = useState(null); @@ -118,6 +118,10 @@ export default function useSearch() { const handleSavePlaylist = async () => {}; + useEffect(() => { + initialLoading(); + }, []); + return { search, setSearch, diff --git a/apps/web/app/hooks/useToSingList.ts b/apps/web/app/hooks/useToSingList.ts index 334fbbe..c97dd05 100644 --- a/apps/web/app/hooks/useToSingList.ts +++ b/apps/web/app/hooks/useToSingList.ts @@ -1,16 +1,17 @@ // hooks/useToSingList.ts import { DragEndEvent } from '@dnd-kit/core'; import { arrayMove } from '@dnd-kit/sortable'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import useAuthStore from '@/stores/useAuthStore'; import useLoadingStore from '@/stores/useLoadingStore'; -import { ToSing } from '@/types/song'; +import useSongStore from '@/stores/useSongStore'; export default function useToSingList() { - const [toSings, setToSings] = useState([]); - const { startLoading, stopLoading } = useLoadingStore(); + const { startLoading, stopLoading, initialLoading } = useLoadingStore(); const { isAuthenticated } = useAuthStore(); + const { toSings, swapToSings, refreshToSings, refreshLikedSongs, refreshRecentSongs } = + useSongStore(); const handleApiCall = async (apiCall: () => Promise, onError?: () => void) => { startLoading(); @@ -28,12 +29,7 @@ export default function useToSingList() { const handleSearch = async () => { await handleApiCall(async () => { - const response = await fetch('/api/songs/tosing'); - const { success, data } = await response.json(); - if (success) { - setToSings(data); - } - return success; + refreshToSings(); }); }; @@ -75,21 +71,21 @@ export default function useToSingList() { }); const { success } = await response.json(); - setToSings(newItems); + swapToSings(newItems); return success; }, handleSearch); }; const handleDelete = async (songId: string) => { await handleApiCall(async () => { - const response = await fetch('/api/songs/tosing', { + await fetch('/api/songs/tosing', { method: 'DELETE', body: JSON.stringify({ songId }), headers: { 'Content-Type': 'application/json' }, }); - const { success } = await response.json(); - setToSings(prev => prev.filter(item => item.songs.id !== songId)); - return success; + swapToSings(toSings.filter(item => item.songs.id !== songId)); + refreshLikedSongs(); + refreshRecentSongs(); }, handleSearch); }; @@ -106,7 +102,7 @@ export default function useToSingList() { headers: { 'Content-Type': 'application/json' }, }); const { success } = await response.json(); - setToSings(newItems); + swapToSings(newItems); return success; }, handleSearch); }; @@ -125,7 +121,7 @@ export default function useToSingList() { headers: { 'Content-Type': 'application/json' }, }); const { success } = await response.json(); - setToSings(newItems); + swapToSings(newItems); return success; }, handleSearch); }; @@ -153,6 +149,7 @@ export default function useToSingList() { body: JSON.stringify({ songId }), headers: { 'Content-Type': 'application/json' }, }), + handleDelete(songId), ]); }, handleSearch); }; @@ -161,11 +158,11 @@ export default function useToSingList() { useEffect(() => { if (isAuthenticated) { handleSearch(); + initialLoading(); } }, [isAuthenticated]); return { - toSings, handleDragEnd, handleSearch, handleDelete, diff --git a/apps/web/app/library/Menu.tsx b/apps/web/app/library/Menu.tsx new file mode 100644 index 0000000..ef74bce --- /dev/null +++ b/apps/web/app/library/Menu.tsx @@ -0,0 +1,35 @@ +import { ArrowLeft } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +interface MenuContentProps { + menuType: 'liked' | 'stats'; +} + +export default function Menu({ menuType }: MenuContentProps) { + const router = useRouter(); + return ( + +
+ +

반갑습니다, 홍길동님

+
+ + +
+

+ {menuType === 'liked' + ? '좋아요 곡 관리 콘텐츠가 여기에 표시됩니다' + : '노래방 통계 콘텐츠가 여기에 표시됩니다'} +

+
+
+
+
+ ); +} diff --git a/apps/web/app/library/liked/page.tsx b/apps/web/app/library/liked/page.tsx new file mode 100644 index 0000000..f1aa687 --- /dev/null +++ b/apps/web/app/library/liked/page.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { ArrowLeft } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +export default function LikedPage() { + const router = useRouter(); + + return ( +
+
+ +

좋아요 곡 관리

+
+ + + + +
+

+ 좋아요 곡 관리 콘텐츠가 여기에 표시됩니다 +

+
+
+
+
+
+ ); +} diff --git a/apps/web/app/library/page.tsx b/apps/web/app/library/page.tsx index fccabb7..80beb23 100644 --- a/apps/web/app/library/page.tsx +++ b/apps/web/app/library/page.tsx @@ -1,8 +1,47 @@ +'use client'; + +import { BarChart2, Heart } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +const menuItems = [ + { + id: 'liked', + title: '좋아요 곡 관리', + description: '좋아요를 누른 노래를 관리합니다', + icon: , + }, + { + id: 'stats', + title: '노래방 통계', + description: '나의 노래방 이용 통계를 확인합니다', + icon: , + }, +]; + export default function LibraryPage() { + const router = useRouter(); + return ( -
-

라이브러리 페이지

-

준비 중입니다...

+
+

반갑습니다, 홍길동님

+ + {menuItems.map(item => ( + router.push(`/library/${item.id}`)} + > + +
{item.icon}
+
+ {item.title} + {item.description} +
+
+
+ ))}
); } diff --git a/apps/web/app/library/stats/page.tsx b/apps/web/app/library/stats/page.tsx new file mode 100644 index 0000000..e8031a6 --- /dev/null +++ b/apps/web/app/library/stats/page.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { ArrowLeft } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +export default function StatsPage() { + const router = useRouter(); + + return ( +
+
+ +

노래방 통계

+
+ + + + +
+

+ 노래방 통계 콘텐츠가 여기에 표시됩니다 +

+
+
+
+
+
+ ); +} diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx new file mode 100644 index 0000000..f40f4fe --- /dev/null +++ b/apps/web/app/not-found.tsx @@ -0,0 +1,44 @@ +import { Home } from 'lucide-react'; +import Link from 'next/link'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +export default function NotFound() { + return ( +
+ + + 페이지를 찾을 수 없어요 + + 요청하신 페이지가 존재하지 않아요 + + + + +
404
+

+ 주소가 올바른지 확인하거나
+ 다른 페이지로 이동해보세요 +

+
+ + + + +
+
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 8235ac9..8fff115 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import Home from '@/home/page'; +import HomePage from '@/home/page'; -export default function HomePage() { - return ; +export default function Home() { + return ; } diff --git a/apps/web/app/popular/RankingList.tsx b/apps/web/app/popular/RankingList.tsx new file mode 100644 index 0000000..295e204 --- /dev/null +++ b/apps/web/app/popular/RankingList.tsx @@ -0,0 +1,66 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { cn } from '@/utils/cn'; + +interface RankingItemProps { + rank: number; + title: string; + artist: string; + + className?: string; +} + +export function RankingItem({ rank, title, artist, className }: RankingItemProps) { + // 등수에 따른 색상 및 스타일 결정 + const getRankStyle = (rank: number) => { + switch (rank) { + case 1: + return 'bg-amber-500 text-white font-bold'; // 금메달 색상 + case 2: + return 'bg-gray-300 text-white font-bold'; // 은메달 색상 + case 3: + return 'bg-amber-700 text-white font-bold'; // 동메달 색상 + default: + return 'bg-muted text-muted-foreground'; + } + }; + + return ( +
+
+ {rank} +
+
+

{title}

+

{artist}

+
+
+ ); +} + +interface RankingListProps { + title: string; + items: Array>; + className?: string; +} + +export default function RankingList({ title, items, className }: RankingListProps) { + return ( + + + {title} + + +
+ {items.map((item, index) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/web/app/popular/page.tsx b/apps/web/app/popular/page.tsx index c0bfe26..1e63896 100644 --- a/apps/web/app/popular/page.tsx +++ b/apps/web/app/popular/page.tsx @@ -1,8 +1,67 @@ +import RankingList from './RankingList'; + +// 샘플 데이터 +const WEEKLY_HOT_SONGS = [ + { rank: 1, title: 'Love Lee', artist: 'AKMU', num_tj: '97531', num_ky: '84612' }, + { rank: 2, title: 'Perfect Night', artist: '르세라핌', num_tj: '97485', num_ky: '84599' }, + { rank: 3, title: 'Drama', artist: 'aespa', num_tj: '97462', num_ky: '84587' }, + { rank: 4, title: 'Smoke', artist: '다이나믹 듀오', num_tj: '97421', num_ky: '84563' }, + { rank: 5, title: 'Seven (feat. Latto)', artist: '정국', num_tj: '97380', num_ky: '84542' }, + { rank: 6, title: 'Super Shy', artist: 'NewJeans', num_tj: '97325', num_ky: '84521' }, + { rank: 7, title: '헤어지자 말해요', artist: '박재정', num_tj: '97289', num_ky: '84503' }, + { rank: 8, title: 'ETA', artist: 'NewJeans', num_tj: '97245', num_ky: '84487' }, + { rank: 9, title: 'Hype Boy', artist: 'NewJeans', num_tj: '97201', num_ky: '84462' }, + { rank: 10, title: '사랑은 늘 도망가', artist: '임영웅', num_tj: '97156', num_ky: '84441' }, +]; + +const MONTHLY_HOT_SONGS = [ + { rank: 1, title: '사랑은 늘 도망가', artist: '임영웅', num_tj: '97156', num_ky: '84441' }, + { rank: 2, title: '모든 날, 모든 순간', artist: '폴킴', num_tj: '96842', num_ky: '84321' }, + { rank: 3, title: '다시 만날 수 있을까', artist: '임영웅', num_tj: '96789', num_ky: '84298' }, + { rank: 4, title: "That's Hilarious", artist: 'Charlie Puth', num_tj: '96745', num_ky: '84276' }, + { rank: 5, title: 'LOVE DIVE', artist: 'IVE', num_tj: '96701', num_ky: '84254' }, + { rank: 6, title: '취중고백', artist: '김민석', num_tj: '96654', num_ky: '84231' }, + { + rank: 7, + title: 'That That', + artist: '싸이 (Prod. & Feat. SUGA of BTS)', + num_tj: '96612', + num_ky: '84209', + }, + { rank: 8, title: 'TOMBOY', artist: '(여자)아이들', num_tj: '96578', num_ky: '84187' }, + { rank: 9, title: '사랑인가 봐', artist: '멜로망스', num_tj: '96534', num_ky: '84165' }, + { + rank: 10, + title: 'GANADARA', + artist: '박재범 (Feat. 아이유)', + num_tj: '96498', + num_ky: '84143', + }, +]; + +const TROT_HOT_SONGS = [ + { rank: 1, title: '이제 나만 믿어요', artist: '임영웅', num_tj: '96321', num_ky: '84098' }, + { rank: 2, title: '별빛 같은 나의 사랑아', artist: '임영웅', num_tj: '96287', num_ky: '84076' }, + { rank: 3, title: '보금자리', artist: '임영웅', num_tj: '96254', num_ky: '84054' }, + { rank: 4, title: '사랑은 늘 도망가', artist: '임영웅', num_tj: '97156', num_ky: '84441' }, + { rank: 5, title: '찐이야', artist: '영탁', num_tj: '96187', num_ky: '84032' }, + { rank: 6, title: '다시 만날 수 있을까', artist: '임영웅', num_tj: '96789', num_ky: '84298' }, + { rank: 7, title: '막걸리 한잔', artist: '임영웅', num_tj: '96121', num_ky: '84010' }, + { rank: 8, title: '당신이 좋은 이유', artist: '설운도', num_tj: '96087', num_ky: '83988' }, + { rank: 9, title: '진또배기', artist: '홍진영', num_tj: '96054', num_ky: '83966' }, + { rank: 10, title: '찐이야', artist: '홍진영', num_tj: '96021', num_ky: '83944' }, +]; + export default function PopularPage() { return ( -
-

인기 페이지

-

준비 중입니다...

+
+

인기 노래

+ +
+ + + +
); } diff --git a/apps/web/app/search/page.tsx b/apps/web/app/search/page.tsx index e501b0f..269b79d 100644 --- a/apps/web/app/search/page.tsx +++ b/apps/web/app/search/page.tsx @@ -4,6 +4,7 @@ import { Mic, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import useSearch from '@/hooks/useSearch'; @@ -33,8 +34,8 @@ export default function SearchPage() { }; return ( -
-
+
+

노래 검색

검색
- -
+ {searchResults.length > 0 ? ( -
+
{searchResults.map((song, index) => (
)} -
- +
{/* {isModal && } */}
); diff --git a/apps/web/app/stores/useLoadingStore.ts b/apps/web/app/stores/useLoadingStore.ts index 4f0847e..b7254dc 100644 --- a/apps/web/app/stores/useLoadingStore.ts +++ b/apps/web/app/stores/useLoadingStore.ts @@ -3,13 +3,16 @@ import { create } from 'zustand'; interface LoadingState { count: number; isLoading: boolean; + isInitialLoading: boolean; startLoading: () => void; stopLoading: () => void; + initialLoading: () => void; } const useLoadingStore = create((set, get) => ({ count: 0, isLoading: false, + isInitialLoading: true, startLoading: () => { const newCount = get().count + 1; set({ count: newCount, isLoading: true }); @@ -21,6 +24,9 @@ const useLoadingStore = create((set, get) => ({ isLoading: newCount > 0, }); }, + initialLoading: () => { + set({ isInitialLoading: false }); + }, })); export default useLoadingStore; diff --git a/apps/web/app/stores/useSongStore.ts b/apps/web/app/stores/useSongStore.ts new file mode 100644 index 0000000..6bb9c45 --- /dev/null +++ b/apps/web/app/stores/useSongStore.ts @@ -0,0 +1,62 @@ +import { create } from 'zustand'; + +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; +} + +const useSongStore = create((set, get) => ({ + toSings: [], + likedSongs: [], + recentSongs: [], + + swapToSings: (toSings: ToSing[]) => { + set({ toSings }); + }, + + refreshToSings: async () => { + const response = await fetch('/api/songs/tosing'); + const { success, data } = await response.json(); + if (success) { + set({ toSings: data }); + } + }, + + refreshLikedSongs: async () => { + const response = await fetch('/api/songs/like'); + const { success, data } = await response.json(); + if (success) { + set({ likedSongs: data }); + } + }, + refreshRecentSongs: async () => { + const response = await fetch('/api/songs/recent'); + const { success, data } = await response.json(); + if (success) { + set({ recentSongs: data }); + } + }, + + postToSingSongs: async (songIds: string[]) => { + const response = await fetch('/api/songs/tosing/array', { + method: 'POST', + body: JSON.stringify({ songIds }), + }); + const { success } = await response.json(); + if (success) { + get().refreshToSings(); + get().refreshLikedSongs(); + get().refreshRecentSongs(); + } + }, +})); + +export default useSongStore; diff --git a/apps/web/package.json b/apps/web/package.json index 51e160e..fd4ad58 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db6bdc6..4a85292 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,9 @@ importers: '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-separator': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1761,6 +1764,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} @@ -1869,6 +1875,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.5': resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} peerDependencies: @@ -2043,6 +2058,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-scroll-area@1.2.4': + resolution: {integrity: sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-separator@1.1.2': resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==} peerDependencies: @@ -9135,6 +9163,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/number@1.1.1': {} + '@radix-ui/primitive@1.1.1': {} '@radix-ui/primitive@1.1.2': {} @@ -9233,6 +9263,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-direction@1.1.1(@types/react@19.0.10)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -9403,6 +9439,23 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-scroll-area@1.2.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)