Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 47 additions & 68 deletions apps/web/src/app/tosing/AddListModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import useAddSongList, { type TabType } from '@/hooks/useAddSongList';
import { useLikeSongQuery } from '@/queries/likeSongQuery';
import { useSaveSongQuery } from '@/queries/saveSongQuery';
Expand All @@ -17,28 +16,31 @@ import useAuthStore from '@/stores/useAuthStore';
import ModalSongItem from './ModalSongItem';

interface AddListModalProps {
type: TabType;
isOpen: boolean;
onClose: () => void;
}

export default function AddListModal({ isOpen, onClose }: AddListModalProps) {
const {
activeTab,
setActiveTab,
songSelected,
handleToggleSelect,
handleConfirmAdd,
totalSelectedCount,
} = useAddSongList();
const MODAL_TITLE: Record<TabType, string> = {
like: '즐겨찾기에서 추가',
save: '재생목록에서 추가',
};

export default function AddListModal({ type, isOpen, onClose }: AddListModalProps) {
const { songSelected, handleToggleSelect, handleConfirmAdd, totalSelectedCount } =
useAddSongList();

const { isAuthenticated } = useAuthStore();

const { data: likedSongs, isLoading: isLoadingLikedSongs } = useLikeSongQuery(isAuthenticated);
const { data: likedSongs, isLoading: isLoadingLikedSongs } = useLikeSongQuery(
isAuthenticated && type === 'like',
);

const { data: saveSongFolders, isLoading: isLoadingSongFolders } =
useSaveSongQuery(isAuthenticated);
const { data: saveSongFolders, isLoading: isLoadingSongFolders } = useSaveSongQuery(
isAuthenticated && type === 'save',
);

const isLoading = isLoadingLikedSongs || isLoadingSongFolders;
const isLoading = type === 'like' ? isLoadingLikedSongs : isLoadingSongFolders;

const handleClickConfirm = () => {
handleConfirmAdd();
Expand All @@ -49,65 +51,42 @@ export default function AddListModal({ isOpen, onClose }: AddListModalProps) {
<Dialog open={isOpen && !isLoading} onOpenChange={open => !open && onClose()}>
<DialogContent className="flex h-[500px] flex-col gap-0 p-0 sm:max-w-[450px]">
<DialogHeader className="p-6 pb-2">
<DialogTitle>플레이리스트에 추가할 노래 선택</DialogTitle>
<DialogTitle>{MODAL_TITLE[type]}</DialogTitle>
</DialogHeader>

<div className="flex flex-1 flex-col overflow-hidden px-6">
<Tabs
value={activeTab}
onValueChange={value => setActiveTab(value as TabType)}
className="flex w-full flex-1 flex-col overflow-y-auto"
>
<div>
<TabsList className="my-2 grid w-full grid-cols-2">
<TabsTrigger value="like">즐겨찾기</TabsTrigger>
<TabsTrigger value="save">재생목록</TabsTrigger>
</TabsList>
</div>

<div className="flex-1 overflow-y-auto">
<TabsContent value="like" className="mt-0 h-full">
<div className="h-full pr-2">
{likedSongs &&
likedSongs.map((song, index) => (
<ModalSongItem
key={song.song_id + 'like' + index}
song={song}
isSelected={songSelected.includes(song.song_id)}
onToggleSelect={handleToggleSelect}
/>
))}
</div>
</TabsContent>
<div className="flex-1 overflow-y-auto px-6">
{type === 'like' &&
likedSongs &&
likedSongs.map((song, index) => (
<ModalSongItem
key={song.song_id + 'like' + index}
song={song}
isSelected={songSelected.includes(song.song_id)}
onToggleSelect={handleToggleSelect}
/>
))}

<TabsContent value="save" className="mt-0 h-full">
<div className="h-full pr-2">
{saveSongFolders &&
saveSongFolders.map((songFolder, index) => {
return (
<div key={songFolder.folder_id + index}>
<div className="flex justify-between border-b text-lg font-bold">
<h3>{songFolder.folder_name}</h3>
{type === 'save' &&
saveSongFolders &&
saveSongFolders.map((songFolder, index) => (
<div key={songFolder.folder_id + index}>
<div className="flex justify-between border-b text-lg font-bold">
<h3>{songFolder.folder_name}</h3>

<span className="text-muted-foreground text-sm">
{songFolder.songList.length}곡
</span>
</div>
{songFolder.songList.map((song, index) => (
<ModalSongItem
key={song.song_id + 'save' + index}
song={song}
isSelected={songSelected.includes(song.song_id)}
onToggleSelect={handleToggleSelect}
/>
))}
</div>
);
})}
<span className="text-muted-foreground text-sm">
{songFolder.songList.length}곡
</span>
</div>
</TabsContent>
</div>
</Tabs>
{songFolder.songList.map((song, index) => (
<ModalSongItem
key={song.song_id + 'save' + index}
song={song}
isSelected={songSelected.includes(song.song_id)}
onToggleSelect={handleToggleSelect}
/>
))}
</div>
))}
</div>

<DialogFooter className="border-t p-6 pt-4">
Expand Down
44 changes: 29 additions & 15 deletions apps/web/src/app/tosing/AddSongButton.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
'use client';

import { AirplayIcon } from 'lucide-react';
import { ListPlus, Star } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';

import { Button } from '@/components/ui/button';
import { type TabType } from '@/hooks/useAddSongList';
import useAuthStore from '@/stores/useAuthStore';

import AddListModal from './AddListModal';

export default function AddSongButton() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [openModal, setOpenModal] = useState<TabType | null>(null);
const { isAuthenticated } = useAuthStore();

const handleAddSong = () => {
const handleOpenModal = (type: TabType) => {
if (!isAuthenticated) {
toast.error('로그인하고 저장한 노래에서 곡을 추가해보세요!');
return;
}
setIsModalOpen(true);
setOpenModal(type);
};

return (
<>
<Button
variant="outline"
size="sm"
className="flex items-center gap-1"
onClick={handleAddSong}
>
<AirplayIcon className="h-4 w-4" />
<span>곡 추가</span>
</Button>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
className="dark:hover:bg-primary dark:hover:text-primary-foreground flex items-center gap-1"
onClick={() => handleOpenModal('like')}
>
<Star className="h-4 w-4" />
<span>즐겨찾기</span>
</Button>
<Button
variant="outline"
size="sm"
className="dark:hover:bg-primary dark:hover:text-primary-foreground flex items-center gap-1"
onClick={() => handleOpenModal('save')}
>
<ListPlus className="h-4 w-4" />
<span>재생목록</span>
</Button>
</div>

{/* 모달도 여기서 렌더링 */}
<AddListModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
{openModal && (
<AddListModal type={openModal} isOpen={!!openModal} onClose={() => setOpenModal(null)} />
)}
</>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/app/tosing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function HomePage() {
return (
<div className="bg-background flex h-full flex-col">
<div className="mb-6 flex shrink-0 items-center justify-between">
<h1 className="text-2xl font-bold">노래방 플레이리스트</h1>
<h1 className="text-2xl font-bold">플레이리스트</h1>
<AddSongButton />
</div>
<ScrollArea className="min-h-0 flex-1 py-4">
Expand Down
16 changes: 0 additions & 16 deletions apps/web/src/components/SongSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
'use client';

import { getPrimaryArtistName } from '@repo/constants';

import MarqueeText from '@/components/MarqueeText';
import SongBadges from '@/components/SongBadges';
import { useCurrentArtistOfMonthQuery } from '@/queries/artistVoteQuery';
import { Song } from '@/types/song';
import { cn } from '@/utils/cn';
import { splitDisplay } from '@/utils/songDisplay';
Expand Down Expand Up @@ -32,12 +29,6 @@ export default function SongSummary({ song, className }: SongSummaryProps) {
const titleParts = splitDisplay(title_ko, title);
const artistParts = splitDisplay(artist_ko, artist);

// songs.artist는 "IU(Feat.최백호)"처럼 원문 그대로라, artists 마스터에 등록된 정규화된
// 이름과 곧이곧대로 비교하면 우승 아티스트의 피처링·듀엣 곡에 배지가 빠진다.
// 맨 앞에 적힌 주 아티스트만 뽑아 비교한다.
const { data: artistOfMonth } = useCurrentArtistOfMonthQuery();
const isArtistOfMonth = !!artistOfMonth && artistOfMonth.artist === getPrimaryArtistName(artist);

return (
<div className={cn('flex w-full items-start justify-between gap-3', className)}>
{/* min-w-0가 없으면 MarqueeText가 부모를 밀어내 번호 영역이 잘린다 */}
Expand All @@ -57,13 +48,6 @@ export default function SongSummary({ song, className }: SongSummaryProps) {
{artistParts.secondary}
</MarqueeText>
)}
{isArtistOfMonth && (
<span className="mt-0.5 inline-flex w-fit items-center gap-1 rounded-full bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-bold text-amber-600 dark:text-amber-400">
{/* month는 'YYYY-MM-DD'(매월 1일)라 앞 7자리만 잘라 쓴다.
"이달"이 언제인지는 곡 카드만 보면 알 수 없어 선정된 달을 함께 적는다. */}
🏆 {artistOfMonth.month.slice(0, 7)} 이달의 아티스트
</span>
)}
</div>

{/* 번호는 자릿수가 달라도 세로로 맞아떨어지도록 고정폭 + tabular-nums */}
Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/hooks/useAddSongList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { usePostToSingSongMutation } from '@/queries/tosingSongQuery';
export type TabType = 'like' | 'save';

export default function useAddSongList() {
const [activeTab, setActiveTab] = useState<TabType>('like');

const [songSelected, setSongSelected] = useState<string[]>([]);

const { mutate: postToSingSong } = usePostToSingSongMutation();
Expand All @@ -27,8 +25,6 @@ export default function useAddSongList() {
const totalSelectedCount = songSelected.length;

return {
activeTab,
setActiveTab,
songSelected,
handleToggleSelect,
handleConfirmAdd,
Expand Down
Loading