token관리 변경
This commit is contained in:
@@ -277,7 +277,16 @@ export function VibePage() {
|
|||||||
const authResponse = await spotifyService.login();
|
const authResponse = await spotifyService.login();
|
||||||
|
|
||||||
if (authResponse) {
|
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에 로그인되었습니다!");
|
Alert.success("Spotify에 로그인되었습니다!");
|
||||||
|
|
||||||
// 백엔드 API를 통해 플레이리스트 목록 가져오기
|
// 백엔드 API를 통해 플레이리스트 목록 가져오기
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import type {
|
|||||||
SpotifyBackendPlaylistsResponse,
|
SpotifyBackendPlaylistsResponse,
|
||||||
SpotifyAddTracksRequest,
|
SpotifyAddTracksRequest,
|
||||||
SpotifyAddTracksResponse,
|
SpotifyAddTracksResponse,
|
||||||
|
SpotifyAuthCallbackRequest,
|
||||||
|
SpotifyAuthCallbackResponse,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spotify OAuth 2.0 서비스
|
* Spotify OAuth 2.0 서비스 (Session 기반)
|
||||||
*/
|
*/
|
||||||
class SpotifyService {
|
class SpotifyService {
|
||||||
private tokenKey = 'spotify_access_token';
|
private sessionIdKey = 'spotify_session_id';
|
||||||
private refreshTokenKey = 'spotify_refresh_token';
|
private sessionExpiresAtKey = 'spotify_session_expires_at';
|
||||||
private expiresAtKey = 'spotify_expires_at';
|
|
||||||
|
|
||||||
// Spotify OAuth 설정
|
// Spotify OAuth 설정
|
||||||
private clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID || '';
|
private clientId = import.meta.env.VITE_SPOTIFY_CLIENT_ID || '';
|
||||||
@@ -165,13 +166,6 @@ class SpotifyService {
|
|||||||
|
|
||||||
const data: SpotifyAuthResponse = await response.json();
|
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_code_verifier');
|
||||||
sessionStorage.removeItem('spotify_state');
|
sessionStorage.removeItem('spotify_state');
|
||||||
@@ -180,96 +174,55 @@ class SpotifyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 플레이리스트 가져오기
|
* OAuth 토큰을 백엔드로 전송하여 세션 생성
|
||||||
*/
|
*/
|
||||||
async getUserPlaylists(limit: number = 50, offset: number = 0): Promise<SpotifyPlaylistsResponse> {
|
async sendAuthCallback(authData: SpotifyAuthResponse): Promise<ApiResponse<SpotifyAuthCallbackResponse>> {
|
||||||
const token = this.getToken();
|
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) {
|
const response = await apiClient.post<SpotifyAuthCallbackResponse>(
|
||||||
throw new Error('로그인이 필요합니다.');
|
'/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');
|
return response;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그아웃
|
* 로그아웃
|
||||||
*/
|
*/
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.removeToken();
|
this.removeSessionId();
|
||||||
this.removeRefreshToken();
|
this.removeSessionExpiresAt();
|
||||||
this.removeExpiresAt();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 백엔드 API를 통해 플레이리스트 목록 가져오기
|
* 백엔드 API를 통해 플레이리스트 목록 가져오기
|
||||||
*/
|
*/
|
||||||
async getPlaylistsFromBackend(): Promise<ApiResponse<SpotifyBackendPlaylistsResponse>> {
|
async getPlaylistsFromBackend(): Promise<ApiResponse<SpotifyBackendPlaylistsResponse>> {
|
||||||
// 토큰 유효성 확인 및 자동 갱신
|
const sessionId = this.getSessionId();
|
||||||
const token = await this.getValidToken();
|
|
||||||
|
|
||||||
if (!token) {
|
if (!sessionId) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '로그인이 필요합니다.',
|
error: '로그인이 필요합니다.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return await apiClient.post<SpotifyBackendPlaylistsResponse>('/v2/spotify/playlists', {
|
return await apiClient.get<SpotifyBackendPlaylistsResponse>('/v2/spotify/playlists', {
|
||||||
access_token: token,
|
headers: {
|
||||||
|
'X-Session-Id': sessionId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,101 +230,58 @@ class SpotifyService {
|
|||||||
* 백엔드 API를 통해 트랙 추가
|
* 백엔드 API를 통해 트랙 추가
|
||||||
*/
|
*/
|
||||||
async addTracksToPlaylist(request: SpotifyAddTracksRequest): Promise<ApiResponse<SpotifyAddTracksResponse>> {
|
async addTracksToPlaylist(request: SpotifyAddTracksRequest): Promise<ApiResponse<SpotifyAddTracksResponse>> {
|
||||||
// 토큰 유효성 확인 및 자동 갱신
|
const sessionId = this.getSessionId();
|
||||||
const validToken = await this.getValidToken();
|
|
||||||
|
|
||||||
if (!validToken) {
|
if (!sessionId) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '로그인이 필요합니다.',
|
error: '로그인이 필요합니다.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유효한 토큰으로 업데이트
|
return await apiClient.post<SpotifyAddTracksResponse>('/v2/spotify/playlists/tracks', request, {
|
||||||
const updatedRequest = {
|
headers: {
|
||||||
...request,
|
'X-Session-Id': sessionId,
|
||||||
access_token: validToken,
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
return await apiClient.post<SpotifyAddTracksResponse>('/v2/spotify/playlists/tracks', updatedRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 상태 확인
|
* 로그인 상태 확인
|
||||||
*/
|
*/
|
||||||
isLoggedIn(): boolean {
|
isLoggedIn(): boolean {
|
||||||
const token = this.getToken();
|
const sessionId = this.getSessionId();
|
||||||
const expiresAt = this.getExpiresAt();
|
const expiresAt = this.getSessionExpiresAt();
|
||||||
|
|
||||||
if (!token || !expiresAt) return false;
|
if (!sessionId || !expiresAt) return false;
|
||||||
|
|
||||||
return Date.now() < expiresAt;
|
return Date.now() < expiresAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Session 관리 메서드
|
||||||
* 유효한 토큰 가져오기 (만료 시 자동 갱신)
|
private setSessionId(sessionId: string): void {
|
||||||
*/
|
localStorage.setItem(this.sessionIdKey, sessionId);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token 관리 메서드
|
getSessionId(): string | null {
|
||||||
private setToken(token: string): void {
|
return localStorage.getItem(this.sessionIdKey);
|
||||||
localStorage.setItem(this.tokenKey, token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): string | null {
|
private removeSessionId(): void {
|
||||||
return localStorage.getItem(this.tokenKey);
|
localStorage.removeItem(this.sessionIdKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeToken(): void {
|
private setSessionExpiresAt(timestamp: number): void {
|
||||||
localStorage.removeItem(this.tokenKey);
|
localStorage.setItem(this.sessionExpiresAtKey, timestamp.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private setRefreshToken(token: string): void {
|
private getSessionExpiresAt(): number | null {
|
||||||
localStorage.setItem(this.refreshTokenKey, token);
|
const value = localStorage.getItem(this.sessionExpiresAtKey);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
return value ? parseInt(value, 10) : null;
|
return value ? parseInt(value, 10) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeExpiresAt(): void {
|
private removeSessionExpiresAt(): void {
|
||||||
localStorage.removeItem(this.expiresAtKey);
|
localStorage.removeItem(this.sessionExpiresAtKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PKCE 유틸리티 메서드
|
// PKCE 유틸리티 메서드
|
||||||
|
|||||||
@@ -161,6 +161,23 @@ export interface SpotifyTrack {
|
|||||||
uri: string;
|
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)
|
// Spotify 플레이리스트 목록 조회 응답 (백엔드 API)
|
||||||
export interface SpotifyBackendPlaylistsResponse {
|
export interface SpotifyBackendPlaylistsResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -170,7 +187,6 @@ export interface SpotifyBackendPlaylistsResponse {
|
|||||||
|
|
||||||
// Spotify 트랙 추가 요청 타입
|
// Spotify 트랙 추가 요청 타입
|
||||||
export interface SpotifyAddTracksRequest {
|
export interface SpotifyAddTracksRequest {
|
||||||
access_token: string;
|
|
||||||
playlist_id?: string | null;
|
playlist_id?: string | null;
|
||||||
playlist_name?: string;
|
playlist_name?: string;
|
||||||
playlist_description?: string;
|
playlist_description?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user