token관리 변경
This commit is contained in:
@@ -277,7 +277,16 @@ export function VibePage() {
|
||||
const authResponse = await spotifyService.login();
|
||||
|
||||
if (authResponse) {
|
||||
console.log(authResponse);
|
||||
console.log("OAuth 응답:", authResponse);
|
||||
|
||||
// 백엔드로 토큰 전송하여 세션 생성
|
||||
const callbackResponse = await spotifyService.sendAuthCallback(authResponse);
|
||||
|
||||
if (!callbackResponse.success) {
|
||||
Alert.error(callbackResponse.error || "세션 생성에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.success("Spotify에 로그인되었습니다!");
|
||||
|
||||
// 백엔드 API를 통해 플레이리스트 목록 가져오기
|
||||
|
||||
@@ -5,16 +5,17 @@ import type {
|
||||
SpotifyBackendPlaylistsResponse,
|
||||
SpotifyAddTracksRequest,
|
||||
SpotifyAddTracksResponse,
|
||||
SpotifyAuthCallbackRequest,
|
||||
SpotifyAuthCallbackResponse,
|
||||
ApiResponse,
|
||||
} from '../types/api';
|
||||
|
||||
/**
|
||||
* Spotify OAuth 2.0 서비스
|
||||
* Spotify OAuth 2.0 서비스 (Session 기반)
|
||||
*/
|
||||
class SpotifyService {
|
||||
private tokenKey = 'spotify_access_token';
|
||||
private refreshTokenKey = 'spotify_refresh_token';
|
||||
private expiresAtKey = 'spotify_expires_at';
|
||||
private sessionIdKey = 'spotify_session_id';
|
||||
private sessionExpiresAtKey = 'spotify_session_expires_at';
|
||||
|
||||
// Spotify OAuth 설정
|
||||
private clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID || '';
|
||||
@@ -165,13 +166,6 @@ class SpotifyService {
|
||||
|
||||
const data: SpotifyAuthResponse = await response.json();
|
||||
|
||||
// Token 저장
|
||||
this.setToken(data.access_token);
|
||||
if (data.refresh_token) {
|
||||
this.setRefreshToken(data.refresh_token);
|
||||
}
|
||||
this.setExpiresAt(Date.now() + data.expires_in * 1000);
|
||||
|
||||
// 세션 스토리지 정리
|
||||
sessionStorage.removeItem('spotify_code_verifier');
|
||||
sessionStorage.removeItem('spotify_state');
|
||||
@@ -180,96 +174,55 @@ class SpotifyService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 플레이리스트 가져오기
|
||||
* OAuth 토큰을 백엔드로 전송하여 세션 생성
|
||||
*/
|
||||
async getUserPlaylists(limit: number = 50, offset: number = 0): Promise<SpotifyPlaylistsResponse> {
|
||||
const token = this.getToken();
|
||||
async sendAuthCallback(authData: SpotifyAuthResponse): Promise<ApiResponse<SpotifyAuthCallbackResponse>> {
|
||||
const request: SpotifyAuthCallbackRequest = {
|
||||
access_token: authData.access_token,
|
||||
token_type: authData.token_type,
|
||||
expires_in: authData.expires_in,
|
||||
refresh_token: authData.refresh_token,
|
||||
scope: authData.scope,
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
throw new Error('로그인이 필요합니다.');
|
||||
const response = await apiClient.post<SpotifyAuthCallbackResponse>(
|
||||
'/v2/spotify/auth/callback',
|
||||
request
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
this.setSessionId(response.data.session_id);
|
||||
this.setSessionExpiresAt(Date.now() + response.data.expires_in * 1000);
|
||||
}
|
||||
|
||||
const url = new URL('https://api.spotify.com/v1/me/playlists');
|
||||
url.searchParams.append('limit', limit.toString());
|
||||
url.searchParams.append('offset', offset.toString());
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// Token 만료 시 갱신 시도
|
||||
await this.refreshAccessToken();
|
||||
return this.getUserPlaylists(limit, offset);
|
||||
}
|
||||
throw new Error('플레이리스트를 가져오는데 실패했습니다.');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access token 갱신
|
||||
*/
|
||||
private async refreshAccessToken(): Promise<void> {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('다시 로그인해주세요.');
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: this.clientId,
|
||||
});
|
||||
|
||||
const response = await fetch('https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logout();
|
||||
throw new Error('다시 로그인해주세요.');
|
||||
}
|
||||
|
||||
const data: SpotifyAuthResponse = await response.json();
|
||||
this.setToken(data.access_token);
|
||||
this.setExpiresAt(Date.now() + data.expires_in * 1000);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
*/
|
||||
logout(): void {
|
||||
this.removeToken();
|
||||
this.removeRefreshToken();
|
||||
this.removeExpiresAt();
|
||||
this.removeSessionId();
|
||||
this.removeSessionExpiresAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* 백엔드 API를 통해 플레이리스트 목록 가져오기
|
||||
*/
|
||||
async getPlaylistsFromBackend(): Promise<ApiResponse<SpotifyBackendPlaylistsResponse>> {
|
||||
// 토큰 유효성 확인 및 자동 갱신
|
||||
const token = await this.getValidToken();
|
||||
const sessionId = this.getSessionId();
|
||||
|
||||
if (!token) {
|
||||
if (!sessionId) {
|
||||
return {
|
||||
success: false,
|
||||
error: '로그인이 필요합니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return await apiClient.post<SpotifyBackendPlaylistsResponse>('/v2/spotify/playlists', {
|
||||
access_token: token,
|
||||
return await apiClient.get<SpotifyBackendPlaylistsResponse>('/v2/spotify/playlists', {
|
||||
headers: {
|
||||
'X-Session-Id': sessionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -277,101 +230,58 @@ class SpotifyService {
|
||||
* 백엔드 API를 통해 트랙 추가
|
||||
*/
|
||||
async addTracksToPlaylist(request: SpotifyAddTracksRequest): Promise<ApiResponse<SpotifyAddTracksResponse>> {
|
||||
// 토큰 유효성 확인 및 자동 갱신
|
||||
const validToken = await this.getValidToken();
|
||||
const sessionId = this.getSessionId();
|
||||
|
||||
if (!validToken) {
|
||||
if (!sessionId) {
|
||||
return {
|
||||
success: false,
|
||||
error: '로그인이 필요합니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// 유효한 토큰으로 업데이트
|
||||
const updatedRequest = {
|
||||
...request,
|
||||
access_token: validToken,
|
||||
};
|
||||
|
||||
return await apiClient.post<SpotifyAddTracksResponse>('/v2/spotify/playlists/tracks', updatedRequest);
|
||||
return await apiClient.post<SpotifyAddTracksResponse>('/v2/spotify/playlists/tracks', request, {
|
||||
headers: {
|
||||
'X-Session-Id': sessionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 상태 확인
|
||||
*/
|
||||
isLoggedIn(): boolean {
|
||||
const token = this.getToken();
|
||||
const expiresAt = this.getExpiresAt();
|
||||
const sessionId = this.getSessionId();
|
||||
const expiresAt = this.getSessionExpiresAt();
|
||||
|
||||
if (!token || !expiresAt) return false;
|
||||
if (!sessionId || !expiresAt) return false;
|
||||
|
||||
return Date.now() < expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효한 토큰 가져오기 (만료 시 자동 갱신)
|
||||
*/
|
||||
private async getValidToken(): Promise<string | null> {
|
||||
const token = this.getToken();
|
||||
const expiresAt = this.getExpiresAt();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 토큰이 만료되었거나 5분 이내에 만료될 예정이면 갱신
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
if (!expiresAt || Date.now() + fiveMinutes >= expiresAt) {
|
||||
console.log('Spotify 토큰이 만료되었거나 곧 만료됩니다. 토큰을 갱신합니다.');
|
||||
|
||||
try {
|
||||
await this.refreshAccessToken();
|
||||
return this.getToken();
|
||||
} catch (error) {
|
||||
console.error('토큰 갱신 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
// Session 관리 메서드
|
||||
private setSessionId(sessionId: string): void {
|
||||
localStorage.setItem(this.sessionIdKey, sessionId);
|
||||
}
|
||||
|
||||
// Token 관리 메서드
|
||||
private setToken(token: string): void {
|
||||
localStorage.setItem(this.tokenKey, token);
|
||||
getSessionId(): string | null {
|
||||
return localStorage.getItem(this.sessionIdKey);
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem(this.tokenKey);
|
||||
private removeSessionId(): void {
|
||||
localStorage.removeItem(this.sessionIdKey);
|
||||
}
|
||||
|
||||
private removeToken(): void {
|
||||
localStorage.removeItem(this.tokenKey);
|
||||
private setSessionExpiresAt(timestamp: number): void {
|
||||
localStorage.setItem(this.sessionExpiresAtKey, timestamp.toString());
|
||||
}
|
||||
|
||||
private setRefreshToken(token: string): void {
|
||||
localStorage.setItem(this.refreshTokenKey, token);
|
||||
}
|
||||
|
||||
private getRefreshToken(): string | null {
|
||||
return localStorage.getItem(this.refreshTokenKey);
|
||||
}
|
||||
|
||||
private removeRefreshToken(): void {
|
||||
localStorage.removeItem(this.refreshTokenKey);
|
||||
}
|
||||
|
||||
private setExpiresAt(timestamp: number): void {
|
||||
localStorage.setItem(this.expiresAtKey, timestamp.toString());
|
||||
}
|
||||
|
||||
private getExpiresAt(): number | null {
|
||||
const value = localStorage.getItem(this.expiresAtKey);
|
||||
private getSessionExpiresAt(): number | null {
|
||||
const value = localStorage.getItem(this.sessionExpiresAtKey);
|
||||
return value ? parseInt(value, 10) : null;
|
||||
}
|
||||
|
||||
private removeExpiresAt(): void {
|
||||
localStorage.removeItem(this.expiresAtKey);
|
||||
private removeSessionExpiresAt(): void {
|
||||
localStorage.removeItem(this.sessionExpiresAtKey);
|
||||
}
|
||||
|
||||
// PKCE 유틸리티 메서드
|
||||
|
||||
@@ -161,6 +161,23 @@ export interface SpotifyTrack {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
// Spotify OAuth 콜백 요청 타입
|
||||
export interface SpotifyAuthCallbackRequest {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token?: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
// Spotify OAuth 콜백 응답 타입
|
||||
export interface SpotifyAuthCallbackResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
session_id: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
// Spotify 플레이리스트 목록 조회 응답 (백엔드 API)
|
||||
export interface SpotifyBackendPlaylistsResponse {
|
||||
success: boolean;
|
||||
@@ -170,7 +187,6 @@ export interface SpotifyBackendPlaylistsResponse {
|
||||
|
||||
// Spotify 트랙 추가 요청 타입
|
||||
export interface SpotifyAddTracksRequest {
|
||||
access_token: string;
|
||||
playlist_id?: string | null;
|
||||
playlist_name?: string;
|
||||
playlist_description?: string;
|
||||
|
||||
Reference in New Issue
Block a user