import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"; import { ArrowLeft, LogIn, Link as LinkIcon, Loader2, AlertCircle, Music, ArrowRight, ChevronRight, ChevronDown } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; import Alert from "../utils/alert"; import vibeService from "../services/vibeService"; import spotifyService from "../services/spotifyService"; import type { VibePlaylist, VibeTrack } from "../types/api"; import { Alert as AlertUI, AlertDescription } from "../components/ui/alert"; import { Checkbox } from "../components/ui/checkbox"; import { ScrollArea } from "../components/ui/scroll-area"; import { Badge } from "../components/ui/badge"; import { Separator } from "../components/ui/separator"; import { ServiceCard } from "../components/features/service/ServiceCard"; import { streamingServices } from "../constants/streamingServices"; import { SpotifyPlaylistSelector } from "../components/features/spotify/SpotifyPlaylistSelector"; import type { SpotifyPlaylist, SpotifyTrackInfo } from "../types/api"; enum TransferStep { LOGIN = "login", SELECT_TRACKS_TREE = "select_tracks_tree", SELECT_TARGET_SERVICE = "select_target_service", SELECT_SPOTIFY_PLAYLIST = "select_spotify_playlist", } // 확장된 플레이리스트 타입 interface ExpandedPlaylist extends VibePlaylist { expanded: boolean; tracks: VibeTrack[]; isLoadingTracks: boolean; } export function VibePage() { const navigate = useNavigate(); // Step 관리 const [currentStep, setCurrentStep] = useState(TransferStep.LOGIN); // 로그인 관련 const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [url, setUrl] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // 플레이리스트 관련 (트리 구조) const [playlists, setPlaylists] = useState([]); // 선택된 플레이리스트 및 트랙 const [selectedPlaylistIds, setSelectedPlaylistIds] = useState>(new Set()); const [selectedTrackIds, setSelectedTrackIds] = useState>(new Set()); // 대상 서비스 const [targetService, setTargetService] = useState(null); // Spotify 관련 const [spotifyPlaylists, setSpotifyPlaylists] = useState([]); const [isLoadingSpotifyPlaylists, setIsLoadingSpotifyPlaylists] = useState(false); const [isAddingTracks, setIsAddingTracks] = useState(false); const handleLogin = async () => { if (!email || !password) { setError("아이디와 비밀번호를 입력해주세요."); return; } setError(null); setIsLoading(true); try { const response = await vibeService.login({ id: email, password }); if (response.success && response.data?.success) { const playlistsData = response.data.playlists.map(p => ({ ...p, expanded: false, tracks: [], isLoadingTracks: false, })); setPlaylists(playlistsData); setCurrentStep(TransferStep.SELECT_TRACKS_TREE); Alert.success(`${playlistsData.length}개의 플레이리스트를 찾았습니다!`); } else { const errorMsg = response.data?.message || response.error || "로그인에 실패했습니다."; setError(errorMsg); Alert.error(errorMsg); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "로그인 중 오류가 발생했습니다."; setError(errorMessage); Alert.error(errorMessage); } finally { setIsLoading(false); } }; const handleUrlSubmit = async () => { if (!url) { setError("플레이리스트 URL을 입력해주세요."); return; } setError(null); setIsLoading(true); try { const response = await vibeService.getPlaylistByUrl({ url }); if (response.success && response.data) { const playlistData = { ...response.data, expanded: false, tracks: [], isLoadingTracks: false, }; setPlaylists([playlistData]); setCurrentStep(TransferStep.SELECT_TRACKS_TREE); Alert.success("플레이리스트를 성공적으로 가져왔습니다!"); } else { setError(response.error || "플레이리스트를 가져오는데 실패했습니다."); Alert.error(response.error || "플레이리스트를 가져오는데 실패했습니다."); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "URL 처리 중 오류가 발생했습니다."; setError(errorMessage); Alert.error(errorMessage); } finally { setIsLoading(false); } }; // 플레이리스트 확장/축소 및 트랙 로드 const handlePlaylistToggle = async (playlistId: string) => { const playlist = playlists.find(p => p.playlist_id === playlistId); if (!playlist) return; // 이미 확장된 경우 축소 if (playlist.expanded) { setPlaylists(prev => prev.map(p => p.playlist_id === playlistId ? { ...p, expanded: false } : p )); return; } // 트랙이 이미 로드된 경우 확장만 if (playlist.tracks.length > 0) { setPlaylists(prev => prev.map(p => p.playlist_id === playlistId ? { ...p, expanded: true } : p )); return; } // 트랙 로드 setPlaylists(prev => prev.map(p => p.playlist_id === playlistId ? { ...p, isLoadingTracks: true } : p )); try { const response = await vibeService.getPlaylistTracks(playlistId); // 401 Unauthorized - 세션 만료 if (response.status === 401) { vibeService.logout(); Alert.error("세션이 만료되었습니다. 다시 로그인해주세요."); setPlaylists([]); setSelectedPlaylistIds(new Set()); setSelectedTrackIds(new Set()); setCurrentStep(TransferStep.LOGIN); return; } if (response.success && response.data?.success) { setPlaylists(prev => prev.map(p => p.playlist_id === playlistId ? { ...p, tracks: response.data!.tracks, expanded: true, isLoadingTracks: false } : p )); } else { const errorMsg = response.data?.message || response.error || "트랙을 가져오는데 실패했습니다."; Alert.error(errorMsg); setPlaylists(prev => prev.map(p => p.playlist_id === playlistId ? { ...p, isLoadingTracks: false } : p )); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "트랙 조회 중 오류가 발생했습니다."; Alert.error(errorMessage); setPlaylists(prev => prev.map(p => p.playlist_id === playlistId ? { ...p, isLoadingTracks: false } : p )); } }; // 플레이리스트 체크/해제 const handlePlaylistCheck = (playlistId: string, e: React.MouseEvent) => { e.stopPropagation(); const playlist = playlists.find(p => p.playlist_id === playlistId); if (!playlist) return; const isChecked = selectedPlaylistIds.has(playlistId); if (isChecked) { // 플레이리스트 체크 해제 - 모든 하위 트랙도 해제 setSelectedPlaylistIds(prev => { const newSet = new Set(prev); newSet.delete(playlistId); return newSet; }); setSelectedTrackIds(prev => { const newSet = new Set(prev); playlist.tracks.forEach(track => newSet.delete(track.track_id)); return newSet; }); } else { // 플레이리스트 체크 - 모든 하위 트랙도 체크 setSelectedPlaylistIds(prev => new Set(prev).add(playlistId)); setSelectedTrackIds(prev => { const newSet = new Set(prev); playlist.tracks.forEach(track => newSet.add(track.track_id)); return newSet; }); } }; // 트랙 체크/해제 const handleTrackCheck = (playlistId: string, trackId: number, e: React.MouseEvent) => { e.stopPropagation(); const playlist = playlists.find(p => p.playlist_id === playlistId); if (!playlist) return; setSelectedTrackIds(prev => { const newSet = new Set(prev); if (newSet.has(trackId)) { newSet.delete(trackId); } else { newSet.add(trackId); } // 플레이리스트의 모든 트랙이 선택되었는지 확인 const allTracksSelected = playlist.tracks.every(t => newSet.has(t.track_id)); setSelectedPlaylistIds(prevPlaylists => { const newPlaylistSet = new Set(prevPlaylists); if (allTracksSelected && playlist.tracks.length > 0) { newPlaylistSet.add(playlistId); } else { newPlaylistSet.delete(playlistId); } return newPlaylistSet; }); return newSet; }); }; const handleGoToServiceSelection = () => { if (selectedTrackIds.size === 0) { Alert.error("최소 1곡 이상 선택해주세요."); return; } setCurrentStep(TransferStep.SELECT_TARGET_SERVICE); }; const handleServiceClick = async (serviceName: string) => { setTargetService(serviceName); if (serviceName === "Spotify") { try { setIsLoading(true); const authResponse = await spotifyService.login(); if (authResponse) { console.log(authResponse); Alert.success("Spotify에 로그인되었습니다!"); // 백엔드 API를 통해 플레이리스트 목록 가져오기 setIsLoadingSpotifyPlaylists(true); const playlistsResponse = await spotifyService.getPlaylistsFromBackend(); if (playlistsResponse.success && playlistsResponse.data) { console.log("Spotify 플레이리스트 응답:", playlistsResponse.data); setSpotifyPlaylists(playlistsResponse.data.playlists); setCurrentStep(TransferStep.SELECT_SPOTIFY_PLAYLIST); } else { Alert.error(playlistsResponse.error || "플레이리스트를 가져오는데 실패했습니다."); } } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Spotify 로그인에 실패했습니다."; Alert.error(errorMessage); } finally { setIsLoading(false); setIsLoadingSpotifyPlaylists(false); } } else { Alert.info(`${serviceName} 연동은 준비 중입니다.`); } }; const handleSpotifyPlaylistSelect = async (playlistId: string) => { await addTracksToSpotify(playlistId, null); }; const handleSpotifyCreateNew = async (playlistName: string, description?: string) => { await addTracksToSpotify(null, playlistName, description); }; const addTracksToSpotify = async ( playlistId: string | null, playlistName?: string | null, playlistDescription?: string ) => { // 중복 클릭 방지 if (isAddingTracks) { return; } try { setIsLoading(true); setIsAddingTracks(true); // 선택된 트랙들을 Spotify API 형식으로 변환 const selectedTracks: SpotifyTrackInfo[] = []; playlists.forEach((playlist) => { playlist.tracks.forEach((track) => { if (selectedTrackIds.has(track.track_id)) { selectedTracks.push({ track_title: track.track_title, artist_name: track.artists.map((a) => a.artist_name).join(', '), album_title: track.album.album_title, }); } }); }); // API 호출 (토큰 유효성 검사는 서비스 내부에서 자동 처리됨) const response = await spotifyService.addTracksToPlaylist({ access_token: '', // 서비스에서 자동으로 유효한 토큰으로 교체됨 playlist_id: playlistId, playlist_name: playlistName || undefined, playlist_description: playlistDescription, is_public: false, tracks: selectedTracks, }); if (response.success && response.data) { const message = playlistId ? `${response.data.added_count}곡을 플레이리스트에 추가했습니다!` : `새 플레이리스트를 생성하고 ${response.data.added_count}곡을 추가했습니다!`; Alert.success(message); // 첫 페이지로 돌아가기 navigate('/'); } else { Alert.error(response.error || "트랙 추가에 실패했습니다."); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "트랙 추가 중 오류가 발생했습니다."; Alert.error(errorMessage); } finally { setIsLoading(false); setIsAddingTracks(false); } }; const availableServices = streamingServices.filter(s => s.name !== "VIBE"); return (
{/* Header */}
{/* Main Content */}
VIBE

VIBE 플레이리스트 이전

{currentStep === TransferStep.LOGIN && "VIBE 플레이리스트를 가져오기 위해 로그인하거나 URL을 입력하세요"} {currentStep === TransferStep.SELECT_TRACKS_TREE && "이전할 플레이리스트와 곡을 선택하세요"} {currentStep === TransferStep.SELECT_TARGET_SERVICE && "이전할 스트리밍 서비스를 선택하세요"} {currentStep === TransferStep.SELECT_SPOTIFY_PLAYLIST && "Spotify 플레이리스트를 선택하거나 새로 생성하세요"}

{/* 에러 메시지 */} {/**/} {/* {error && (*/} {/* */} {/* */} {/* */} {/* {error}*/} {/* */} {/*
*/} {/* )}*/} {/**/} {/* Step 1: 로그인 카드 */} {currentStep === TransferStep.LOGIN && ( VIBE 플레이리스트 가져오기 로그인 또는 플레이리스트 URL을 통해 연결할 수 있습니다 로그인 URL 입력
setEmail(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLogin()} className="h-10 sm:h-11 text-sm sm:text-base" />
setPassword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLogin()} className="h-10 sm:h-11 text-sm sm:text-base" />
setUrl(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleUrlSubmit()} className="h-10 sm:h-11 text-sm sm:text-base" />
)}
{/* Step 2: 플레이리스트 및 트랙 트리 */} {currentStep === TransferStep.SELECT_TRACKS_TREE && playlists.length > 0 && (
플레이리스트 및 트랙 선택
{selectedTrackIds.size > 0 && ( )}
{selectedTrackIds.size > 0 && (
선택된 트랙 {selectedTrackIds.size}곡
)}
{playlists.map((playlist, index) => { const isPlaylistChecked = selectedPlaylistIds.has(playlist.playlist_id); const hasSelectedTracks = playlist.tracks.some(t => selectedTrackIds.has(t.track_id)); return ( {/* 플레이리스트 아이템 */}
handlePlaylistToggle(playlist.playlist_id)} > {/* 확장/축소 아이콘 */}
{playlist.isLoadingTracks ? ( ) : playlist.expanded ? ( ) : ( )}
{/* 체크박스 */} handlePlaylistCheck(playlist.playlist_id, e)} className="data-[state=checked]:bg-[#FC3C44] data-[state=checked]:border-[#FC3C44] shrink-0" /> {/* 플레이리스트 이미지 */}
{playlist.image_url ? ( {playlist.playlist_name} { e.currentTarget.style.display = 'none'; e.currentTarget.nextElementSibling?.classList.remove('hidden'); }} /> ) : null}
{/* 플레이리스트 정보 */}

{playlist.playlist_name}

{playlist.track_count}곡
{/* 트랙 목록 (확장 시) */} {playlist.expanded && playlist.tracks.length > 0 && (
{playlist.tracks.map((track, trackIndex) => { const isTrackChecked = selectedTrackIds.has(track.track_id); return (
{/* 연결선 */}
handleTrackCheck(playlist.playlist_id, track.track_id, e)} > {/* 체크박스 */} handleTrackCheck(playlist.playlist_id, track.track_id, e)} className="data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0" /> {/* 앨범 이미지 */}
{track.album.image_url ? ( {track.album.album_title} { e.currentTarget.style.display = 'none'; e.currentTarget.nextElementSibling?.classList.remove('hidden'); }} /> ) : null}
{/* 트랙 정보 */}

{track.track_title}

{track.artists.map(a => a.artist_name).join(', ')}

{/* 재생 시간 */}
{track.play_time}
); })}
)}
); })}
)}
{/* Step 3: 대상 스트리밍 서비스 선택 */} {currentStep === TransferStep.SELECT_TARGET_SERVICE && (
이전할 서비스 선택 선택한 {selectedTrackIds.size}곡을 이전할 스트리밍 서비스를 선택하세요
{availableServices.map((service, index) => ( handleServiceClick(service.name)} index={index} /> ))}
)}
{/* Step 4: Spotify 플레이리스트 선택 */} {currentStep === TransferStep.SELECT_SPOTIFY_PLAYLIST && (
플레이리스트 선택 선택한 {selectedTrackIds.size}곡을 추가할 Spotify 플레이리스트를 선택하거나 새로 생성하세요
)}
); }