token관리 변경

This commit is contained in:
2025-12-03 16:05:40 +09:00
parent 4a5dcba74b
commit 9336cac8c1
3 changed files with 81 additions and 146 deletions

View File

@@ -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를 통해 플레이리스트 목록 가져오기

View File

@@ -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 유틸리티 메서드

View File

@@ -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;