diff --git a/apps/web/src/app/recent/page.tsx b/apps/web/src/app/recent/page.tsx
index 707ceec..7e68040 100644
--- a/apps/web/src/app/recent/page.tsx
+++ b/apps/web/src/app/recent/page.tsx
@@ -5,13 +5,19 @@ import { useState } from 'react';
import StaticLoading from '@/components/StaticLoading';
import { Button } from '@/components/ui/button';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
import { useRecentAddSongQuery } from '@/queries/recentAddSongQuery';
import RecentSongCard from './RecentSongCard';
-export default function LibraryPage() {
+export default function RecentSongPage() {
const [today, setToday] = useState(new Date());
- const [prevAction, setPrevAction] = useState<'prev' | 'next' | null>(null);
const { data: recentAddSongs, isLoading: isLoadingRecentAddSongs } = useRecentAddSongQuery(
today.getFullYear(),
@@ -20,40 +26,60 @@ export default function LibraryPage() {
const handlePrevMonth = () => {
setToday(new Date(today.getFullYear(), today.getMonth() - 1, 1));
- setPrevAction('prev');
};
const handleNextMonth = () => {
setToday(new Date(today.getFullYear(), today.getMonth() + 1, 1));
- setPrevAction('next');
};
+ const handleYearChange = (year: string) => {
+ setToday(new Date(Number(year), today.getMonth(), 1));
+ };
+
+ const handleMonthChange = (month: string) => {
+ setToday(new Date(today.getFullYear(), Number(month), 1));
+ };
+
+ const years = Array.from({ length: 20 }, (_, i) => new Date().getFullYear() - i);
+ const months = Array.from({ length: 12 }, (_, i) => i);
+
return (
-
diff --git a/apps/web/src/app/search/AddFolderModal.tsx b/apps/web/src/app/search/AddFolderModal.tsx
index 7feeeb2..6f6ad0a 100644
--- a/apps/web/src/app/search/AddFolderModal.tsx
+++ b/apps/web/src/app/search/AddFolderModal.tsx
@@ -3,6 +3,7 @@
import { CheckCircle, PlusCircle, Save } from 'lucide-react';
import { useEffect, useState } from 'react';
+import MarqueeText from '@/components/MarqueeText';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -112,8 +113,8 @@ export default function AddFolderModal({
{/* 곡 정보 */}
-
{title}
-
{artist}
+
{title}
+
{artist}
diff --git a/apps/web/src/app/search/HomePage.tsx b/apps/web/src/app/search/HomePage.tsx
index b1fb896..bf930c8 100644
--- a/apps/web/src/app/search/HomePage.tsx
+++ b/apps/web/src/app/search/HomePage.tsx
@@ -62,8 +62,6 @@ export default function SearchPage() {
rootMargin: '0px 0px 800px 0px', // 스크롤 하단 600px 이전에 미리 로딩
});
- console.log('inView', inView);
-
const { guestToSingSongs } = useGuestToSingStore();
const isToSing = (song: SearchSong, songId: string) => {
diff --git a/apps/web/src/app/search/MusicCard.tsx b/apps/web/src/app/search/MusicCard.tsx
index 5ff07fc..cabb079 100644
--- a/apps/web/src/app/search/MusicCard.tsx
+++ b/apps/web/src/app/search/MusicCard.tsx
@@ -1,3 +1,5 @@
+import MarqueeText from '@/components/MarqueeText';
+
type MusicCardProps = {
title: string;
artist: string;
@@ -11,16 +13,16 @@ export function MusicCard({ title, artist, reason, onClick }: MusicCardProps) {
onClick(title)}
- className="mb-1 cursor-pointer text-left text-sm font-semibold transition-colors hover:text-blue-500 hover:underline"
+ className="mb-1 w-full cursor-pointer text-left text-sm font-semibold transition-colors hover:text-blue-500 hover:underline"
>
- {title}
+ {title}
onClick(artist)}
- className="text-muted-foreground mb-2 cursor-pointer text-left text-xs transition-colors hover:text-blue-500 hover:underline"
+ className="text-muted-foreground mb-2 w-full cursor-pointer text-left text-xs transition-colors hover:text-blue-500 hover:underline"
>
- {artist}
+ {artist}
{reason}
diff --git a/apps/web/src/app/search/SearchResultCard.tsx b/apps/web/src/app/search/SearchResultCard.tsx
index 1709730..51144df 100644
--- a/apps/web/src/app/search/SearchResultCard.tsx
+++ b/apps/web/src/app/search/SearchResultCard.tsx
@@ -1,11 +1,21 @@
-import { Heart, ListPlus, ListRestart, MinusCircle, PlusCircle, ThumbsUp } from 'lucide-react';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ ChevronDown,
+ Heart,
+ ListPlus,
+ ListRestart,
+ MinusCircle,
+ PlusCircle,
+ ThumbsUp,
+} from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
+import MarqueeText from '@/components/MarqueeText';
import ThumbUpModal from '@/components/ThumbUpModal';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
-import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
+import { Dialog, DialogContent } from '@/components/ui/dialog';
import useAuthStore from '@/stores/useAuthStore';
import { SearchSong } from '@/types/song';
@@ -14,6 +24,7 @@ interface IProps {
isToSing: boolean;
isLike: boolean;
isSave: boolean;
+
onToggleToSing: () => void;
onToggleLike: () => void;
onClickSave: () => void;
@@ -25,15 +36,18 @@ export default function SearchResultCard({
isToSing,
isLike,
isSave,
+
onToggleToSing,
onToggleLike,
onClickSave,
onClickArtist,
}: IProps) {
- const { id, title, artist, num_tj, num_ky } = song;
+ const { id, title, artist, num_tj, num_ky, thumb } = song;
+
const { isAuthenticated } = useAuthStore();
const [open, setOpen] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false);
const handleClickThumbsUp = () => {
if (!isAuthenticated) {
@@ -46,86 +60,119 @@ export default function SearchResultCard({
return (
{/* 메인 콘텐츠 영역 */}
-
+
{/* 노래 정보 */}
-
+
{/* 제목 및 가수 */}
-
-
-
{title}
-
+
+ {title}
+
{artist}
-
+
{/* 노래방 번호 */}
-
-
-
- TJ
- {num_tj}
-
-
-
금영
-
{num_ky}
+
+
+
+ TJ
+ {num_tj}
+
+
+ 금영
+ {num_ky}
+
+
+
setIsExpanded(!isExpanded)}
+ >
+
+
- {/* 버튼 영역 - 우측 하단에 고정 */}
-
-
- {isToSing ? : }
- {isToSing ? '부를곡 취소' : '부를곡 추가'}
-
-
-
-
- {isLike ? '좋아요 취소' : '좋아요'}
-
-
-
- {isSave ? : }
- {isSave ? '재생목록 수정' : '재생목록 추가'}
-
-
+ {/* 버튼 영역 - 애니메이션 적용 */}
+
+ {isExpanded && (
+
+
+
+ {isToSing ? : }
+ {isToSing ? '부를곡 취소' : '부를곡 추가'}
+
+
+
+
+ {isLike ? '좋아요 취소' : '좋아요'}
+
+
+
+ {isSave ? : }
+ {isSave ? '재생목록 수정' : '재생목록 추가'}
+
+
+
+ )}
+
);
diff --git a/apps/web/src/app/tosing/ModalSongItem.tsx b/apps/web/src/app/tosing/ModalSongItem.tsx
index f5afa7f..bdf0c53 100644
--- a/apps/web/src/app/tosing/ModalSongItem.tsx
+++ b/apps/web/src/app/tosing/ModalSongItem.tsx
@@ -1,3 +1,4 @@
+import MarqueeText from '@/components/MarqueeText';
import { Checkbox } from '@/components/ui/checkbox';
import { AddListModalSong } from '@/types/song';
import { cn } from '@/utils/cn';
@@ -26,8 +27,8 @@ export default function ModalSongItem({
disabled={song.isInToSingList}
/>
-
{song.title}
-
{song.artist}
+
{song.title}
+
{song.artist}
);
diff --git a/apps/web/src/app/tosing/SongCard.tsx b/apps/web/src/app/tosing/SongCard.tsx
index de5d74f..74f051b 100644
--- a/apps/web/src/app/tosing/SongCard.tsx
+++ b/apps/web/src/app/tosing/SongCard.tsx
@@ -4,6 +4,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ChevronsDown, ChevronsUp, GripVertical, Trash } from 'lucide-react';
+import MarqueeText from '@/components/MarqueeText';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Song } from '@/types/song';
@@ -31,15 +32,15 @@ export default function SongCard({ song, onDelete, onMoveToTop, onMoveToBottom }
{/* 메인 콘텐츠 영역 */}
{/* 노래 정보 */}
-
+
{/* 제목 및 가수 */}
-
{title}
-
{artist}
+
{title}
+
{artist}
{/* 노래방 번호 */}
-
+
TJ
{num_tj}
diff --git a/apps/web/src/components/MarqueeText.tsx b/apps/web/src/components/MarqueeText.tsx
new file mode 100644
index 0000000..a8abb1f
--- /dev/null
+++ b/apps/web/src/components/MarqueeText.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import { ReactNode, useEffect, useRef, useState } from 'react';
+
+import { cn } from '@/utils/cn';
+
+interface MarqueeTextProps {
+ children: ReactNode;
+ className?: string;
+ onClick?: () => void;
+}
+
+export default function MarqueeText({ children, className, onClick }: MarqueeTextProps) {
+ const containerRef = useRef
(null);
+ const [isOverflowing, setIsOverflowing] = useState(false);
+
+ const checkOverflow = () => {
+ if (containerRef.current) {
+ const { scrollWidth, clientWidth } = containerRef.current;
+ setIsOverflowing(scrollWidth > clientWidth);
+ }
+ };
+
+ useEffect(() => {
+ checkOverflow();
+ const resizeObserver = new ResizeObserver(() => checkOverflow());
+ if (containerRef.current) resizeObserver.observe(containerRef.current);
+ return () => resizeObserver.disconnect();
+ }, [children]);
+
+ return (
+
+
+ {children}
+ {/* 오버플로우 시에만 복제본을 렌더링 */}
+ {isOverflowing && (
+
+ {children}
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/components/RankingItem.tsx b/apps/web/src/components/RankingItem.tsx
index 0d62d58..e27452d 100644
--- a/apps/web/src/components/RankingItem.tsx
+++ b/apps/web/src/components/RankingItem.tsx
@@ -1,15 +1,39 @@
+import { ThumbsUp } from 'lucide-react';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+import MarqueeText from '@/components/MarqueeText';
+import ThumbUpModal from '@/components/ThumbUpModal';
+import { Button } from '@/components/ui/button';
+import { Dialog, DialogContent } from '@/components/ui/dialog';
+import useAuthStore from '@/stores/useAuthStore';
import { cn } from '@/utils/cn';
interface RankingItemProps {
+ id: string;
rank: number;
title: string;
artist: string;
+ num_tj: string;
+ num_ky: string;
value: number;
className?: string;
}
-export default function RankingItem({ rank, title, artist, value, className }: RankingItemProps) {
- // 등수에 따른 색상 및 스타일 결정
+export default function RankingItem({
+ id,
+ rank,
+ title,
+ artist,
+ num_tj,
+ num_ky,
+ value,
+ className,
+}: RankingItemProps) {
+ const { isAuthenticated } = useAuthStore();
+
+ const [open, setOpen] = useState(false);
+
const getRankStyle = (rank: number) => {
switch (rank) {
case 1:
@@ -23,8 +47,16 @@ export default function RankingItem({ rank, title, artist, value, className }: R
}
};
+ const handleClickThumbsUp = () => {
+ if (!isAuthenticated) {
+ toast.error('로그인하고 곡 추천 기능을 사용해보세요!');
+ return;
+ }
+ setOpen(true);
+ };
+
return (
-
+
{rank}
-
-
-
{title}
-
{artist}
+
+
+
+ {title}
+ {artist}
+
+
+
+
+ TJ
+ {num_tj}
+
+
+ 금영
+ {num_ky}
+
+
+
-
{value}회
+
diff --git a/apps/web/src/components/ThumbUpModal.tsx b/apps/web/src/components/ThumbUpModal.tsx
index 47a1557..300625a 100644
--- a/apps/web/src/components/ThumbUpModal.tsx
+++ b/apps/web/src/components/ThumbUpModal.tsx
@@ -17,10 +17,19 @@ import FallingIcons from './FallingIcons';
interface ThumbUpModalProps {
songId: string;
+ title: string;
+ artist: string;
+ thumb: number;
handleClose: () => void;
}
-export default function ThumbUpModal({ songId, handleClose }: ThumbUpModalProps) {
+export default function ThumbUpModal({
+ songId,
+ title,
+ artist,
+ thumb,
+ handleClose,
+}: ThumbUpModalProps) {
const [value, setValue] = useState([0]);
const { data: user } = useUserQuery();
@@ -52,9 +61,29 @@ export default function ThumbUpModal({ songId, handleClose }: ThumbUpModalProps)
-
-
-
+
+ {/* 레이블 추가로 가독성 향상 */}
+
+ Total Points
+
+
+
+
+
+
{
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY as string, {
diff --git a/apps/web/src/queries/songThumbQuery.ts b/apps/web/src/queries/songThumbQuery.ts
index 68fb4c9..2978ab3 100644
--- a/apps/web/src/queries/songThumbQuery.ts
+++ b/apps/web/src/queries/songThumbQuery.ts
@@ -24,6 +24,7 @@ export const useSongThumbMutation = () => {
mutationFn: (body: { songId: string; point: number }) => patchSongThumb(body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['songThumb'] });
+ queryClient.invalidateQueries({ queryKey: ['searchSong'] });
},
onError: error => {
console.error('error', error);
diff --git a/apps/web/src/types/song.ts b/apps/web/src/types/song.ts
index ce60b76..dbb66ab 100644
--- a/apps/web/src/types/song.ts
+++ b/apps/web/src/types/song.ts
@@ -5,6 +5,7 @@ export interface Song {
num_tj: string;
num_ky: string;
+ thumb?: number;
release?: string;
created_at?: string;
}