token관리 추가
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package com.domain.api;
|
||||
|
||||
import com.domain.request.SpotifyAddTracksRequest;
|
||||
import com.domain.request.SpotifyOAuthCallbackRequest;
|
||||
import com.domain.request.SpotifyTokenRequest;
|
||||
import com.domain.request.VibeToSpotifySearchRequest;
|
||||
import com.domain.response.SpotifyAddTracksResponse;
|
||||
import com.domain.response.SpotifyAuthResponse;
|
||||
import com.domain.response.SpotifyPlaylistResponse;
|
||||
import com.domain.response.VibeToSpotifySearchResponse;
|
||||
import com.domain.service.SpotifyService;
|
||||
@@ -23,28 +25,87 @@ public class SpotifyController {
|
||||
|
||||
private final SpotifyService spotifyService;
|
||||
|
||||
|
||||
/**
|
||||
* Spotify Access Token을 사용하여 사용자의 플레이리스트를 조회합니다.
|
||||
* Spotify OAuth2.0 콜백 처리: 토큰을 저장하고 세션 ID를 반환합니다.
|
||||
*
|
||||
* @param request Spotify Access Token
|
||||
* @return Spotify 플레이리스트 목록
|
||||
* @param request OAuth 콜백 요청 (토큰 정보 포함)
|
||||
* @return 인증 응답 (세션 ID 포함)
|
||||
*/
|
||||
@PostMapping("/playlists")
|
||||
@Operation(summary = "Spotify 플레이리스트 조회", description = "Spotify Access Token으로 사용자의 플레이리스트를 조회합니다.")
|
||||
public ResponseEntity<SpotifyPlaylistResponse> getSpotifyPlaylists(@RequestBody SpotifyTokenRequest request) {
|
||||
log.info("Spotify 플레이리스트 조회 요청");
|
||||
@PostMapping("/auth/callback")
|
||||
@Operation(summary = "Spotify OAuth 콜백 처리", description = "OAuth2.0 인증 후 받은 토큰을 저장하고 세션 ID를 반환합니다.")
|
||||
public ResponseEntity<SpotifyAuthResponse> handleOAuthCallback(@RequestBody SpotifyOAuthCallbackRequest request) {
|
||||
log.info("Spotify OAuth 콜백 처리 요청");
|
||||
|
||||
// 입력 검증
|
||||
if (request.getAccessToken() == null || request.getAccessToken().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(SpotifyPlaylistResponse.builder()
|
||||
.body(SpotifyAuthResponse.builder()
|
||||
.success(false)
|
||||
.message("Access Token이 필요합니다.")
|
||||
.build());
|
||||
}
|
||||
|
||||
if (request.getRefreshToken() == null || request.getRefreshToken().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(SpotifyAuthResponse.builder()
|
||||
.success(false)
|
||||
.message("Refresh Token이 필요합니다.")
|
||||
.build());
|
||||
}
|
||||
|
||||
if (request.getExpiresIn() == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(SpotifyAuthResponse.builder()
|
||||
.success(false)
|
||||
.message("Expires In이 필요합니다.")
|
||||
.build());
|
||||
}
|
||||
|
||||
try {
|
||||
SpotifyPlaylistResponse response = spotifyService.getUserPlaylists(request.getAccessToken());
|
||||
SpotifyAuthResponse response = spotifyService.handleOAuthCallback(request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("Spotify OAuth 콜백 처리 성공 - 세션 ID 생성 완료");
|
||||
return ResponseEntity.ok(response);
|
||||
} else {
|
||||
log.warn("Spotify OAuth 콜백 처리 실패 - 메시지: {}", response.getMessage());
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Spotify OAuth 콜백 처리 중 예외 발생", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(SpotifyAuthResponse.builder()
|
||||
.success(false)
|
||||
.message("서버 오류가 발생했습니다: " + e.getMessage())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 ID를 사용하여 사용자의 플레이리스트를 조회합니다.
|
||||
*
|
||||
* @param sessionId 세션 ID (헤더)
|
||||
* @return Spotify 플레이리스트 목록
|
||||
*/
|
||||
@GetMapping("/playlists")
|
||||
@Operation(summary = "Spotify 플레이리스트 조회", description = "세션 ID로 사용자의 플레이리스트를 조회합니다.")
|
||||
public ResponseEntity<SpotifyPlaylistResponse> getSpotifyPlaylists(
|
||||
@RequestHeader("X-Session-Id") String sessionId) {
|
||||
log.info("Spotify 플레이리스트 조회 요청 - 세션 ID: {}", sessionId);
|
||||
|
||||
// 입력 검증
|
||||
if (sessionId == null || sessionId.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(SpotifyPlaylistResponse.builder()
|
||||
.success(false)
|
||||
.message("세션 ID가 필요합니다.")
|
||||
.build());
|
||||
}
|
||||
|
||||
try {
|
||||
SpotifyPlaylistResponse response = spotifyService.getUserPlaylistsWithSession(sessionId);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("Spotify 플레이리스트 조회 성공 - 플레이리스트 개수: {}",
|
||||
@@ -66,22 +127,25 @@ public class SpotifyController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 트랙을 플레이리스트에 추가하거나 새 플레이리스트를 생성합니다.
|
||||
* 세션 ID를 사용하여 트랙을 플레이리스트에 추가하거나 새 플레이리스트를 생성합니다.
|
||||
*
|
||||
* @param request 트랙 추가 요청 (access_token, playlist_id, track_uris 등)
|
||||
* @param sessionId 세션 ID (헤더)
|
||||
* @param request 트랙 추가 요청 (playlist_id, track_uris 등)
|
||||
* @return 트랙 추가 결과
|
||||
*/
|
||||
@PostMapping("/playlists/tracks")
|
||||
@Operation(summary = "Spotify 트랙 추가", description = "기존 플레이리스트에 트랙을 추가하거나 새 플레이리스트를 생성합니다. playlist_id가 없으면 새로 생성합니다.")
|
||||
public ResponseEntity<SpotifyAddTracksResponse> addTracksToPlaylist(@RequestBody SpotifyAddTracksRequest request) {
|
||||
log.info("Spotify 트랙 추가 요청 - 플레이리스트 ID: {}", request.getPlaylistId());
|
||||
public ResponseEntity<SpotifyAddTracksResponse> addTracksToPlaylist(
|
||||
@RequestHeader("X-Session-Id") String sessionId,
|
||||
@RequestBody SpotifyAddTracksRequest request) {
|
||||
log.info("Spotify 트랙 추가 요청 - 플레이리스트 ID: {}, 세션 ID: {}", request.getPlaylistId(), sessionId);
|
||||
|
||||
// 입력 검증
|
||||
if (request.getAccessToken() == null || request.getAccessToken().trim().isEmpty()) {
|
||||
if (sessionId == null || sessionId.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(SpotifyAddTracksResponse.builder()
|
||||
.success(false)
|
||||
.message("Access Token이 필요합니다.")
|
||||
.message("세션 ID가 필요합니다.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -104,7 +168,7 @@ public class SpotifyController {
|
||||
}
|
||||
|
||||
try {
|
||||
SpotifyAddTracksResponse response = spotifyService.addTracksToPlaylist(request);
|
||||
SpotifyAddTracksResponse response = spotifyService.addTracksToPlaylistWithSession(sessionId, request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("Spotify 트랙 추가 성공 - 플레이리스트 ID: {}, 추가된 트랙 수: {}",
|
||||
@@ -126,25 +190,27 @@ public class SpotifyController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibe 트랙 정보를 Spotify에서 검색하여 매칭합니다.
|
||||
* 세션 ID를 사용하여 Vibe 트랙 정보를 Spotify에서 검색하여 매칭합니다.
|
||||
*
|
||||
* @param sessionId 세션 ID (헤더)
|
||||
* @param request Vibe 트랙 정보 리스트 (가수, 제목)
|
||||
* @return Spotify 검색 결과 (track_uri 포함)
|
||||
*/
|
||||
@PostMapping("/search/vibe-tracks")
|
||||
@Operation(summary = "Vibe 트랙 Spotify 검색", description = "Vibe에서 받은 트랙 정보(가수, 제목)를 Spotify에서 검색하여 track_uri를 반환합니다.")
|
||||
public ResponseEntity<VibeToSpotifySearchResponse> searchVibeTracksInSpotify(
|
||||
@RequestHeader("X-Session-Id") String sessionId,
|
||||
@RequestBody VibeToSpotifySearchRequest request) {
|
||||
|
||||
log.info("Vibe 트랙 Spotify 검색 요청 - 트랙 개수: {}",
|
||||
request.getTracks() != null ? request.getTracks().size() : 0);
|
||||
log.info("Vibe 트랙 Spotify 검색 요청 - 트랙 개수: {}, 세션 ID: {}",
|
||||
request.getTracks() != null ? request.getTracks().size() : 0, sessionId);
|
||||
|
||||
// 입력 검증
|
||||
if (request.getAccessToken() == null || request.getAccessToken().trim().isEmpty()) {
|
||||
if (sessionId == null || sessionId.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(VibeToSpotifySearchResponse.builder()
|
||||
.success(false)
|
||||
.message("Access Token이 필요합니다.")
|
||||
.message("세션 ID가 필요합니다.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -157,7 +223,7 @@ public class SpotifyController {
|
||||
}
|
||||
|
||||
try {
|
||||
VibeToSpotifySearchResponse response = spotifyService.searchTracksFromVibe(request);
|
||||
VibeToSpotifySearchResponse response = spotifyService.searchTracksFromVibeWithSession(sessionId, request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("Vibe 트랙 Spotify 검색 완료 - 전체: {}, 매칭: {}, 미매칭: {}",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.domain.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SpotifyOAuthCallbackRequest {
|
||||
|
||||
/**
|
||||
* OAuth2.0 인증 후 받은 토큰 정보
|
||||
*/
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
@JsonProperty("token_type")
|
||||
private String tokenType;
|
||||
|
||||
@JsonProperty("expires_in")
|
||||
private Integer expiresIn;
|
||||
|
||||
@JsonProperty("refresh_token")
|
||||
private String refreshToken;
|
||||
|
||||
@JsonProperty("scope")
|
||||
private String scope;
|
||||
|
||||
/**
|
||||
* 사용자 식별을 위한 암호화된 userId (선택)
|
||||
* 없으면 Spotify API로 사용자 정보 조회
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
37
src/main/java/com/domain/response/SpotifyAuthResponse.java
Normal file
37
src/main/java/com/domain/response/SpotifyAuthResponse.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.domain.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class SpotifyAuthResponse {
|
||||
|
||||
private boolean success;
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 암호화된 세션 ID (userId 암호화)
|
||||
*/
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 토큰 만료 시간 (초)
|
||||
*/
|
||||
@JsonProperty("expires_in")
|
||||
private Integer expiresIn;
|
||||
|
||||
/**
|
||||
* Spotify 사용자 ID
|
||||
*/
|
||||
@JsonProperty("spotify_user_id")
|
||||
private String spotifyUserId;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.domain.service;
|
||||
|
||||
import com.domain.request.SpotifyAddTracksRequest;
|
||||
import com.domain.request.SpotifyOAuthCallbackRequest;
|
||||
import com.domain.request.VibeToSpotifySearchRequest;
|
||||
import com.domain.response.SpotifyAddTracksResponse;
|
||||
import com.domain.response.SpotifyAuthResponse;
|
||||
import com.domain.response.SpotifyPlaylistResponse;
|
||||
import com.domain.response.VibeToSpotifySearchResponse;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -34,6 +36,8 @@ public class SpotifyService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TranslationService translationService;
|
||||
private final SpotifyTokenService spotifyTokenService;
|
||||
private final com.global.util.EncryptionUtil encryptionUtil;
|
||||
|
||||
private static final String SPOTIFY_API_BASE_URL = "https://api.spotify.com/v1";
|
||||
private static final String SPOTIFY_ME_PLAYLISTS_URL = SPOTIFY_API_BASE_URL + "/me/playlists?limit=50";
|
||||
@@ -366,8 +370,200 @@ public class SpotifyService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth2.0 콜백 처리: 토큰을 Redis에 저장하고 암호화된 세션 ID를 반환합니다.
|
||||
*
|
||||
* @param request OAuth 콜백 요청 (토큰 정보 포함)
|
||||
* @return 인증 응답 (세션 ID 포함)
|
||||
*/
|
||||
public SpotifyAuthResponse handleOAuthCallback(SpotifyOAuthCallbackRequest request) {
|
||||
try {
|
||||
// 1. Spotify API로 사용자 ID 조회
|
||||
String spotifyUserId = getUserId(request.getAccessToken());
|
||||
if (spotifyUserId == null) {
|
||||
return SpotifyAuthResponse.builder()
|
||||
.success(false)
|
||||
.message("Spotify 사용자 정보 조회 실패")
|
||||
.build();
|
||||
}
|
||||
|
||||
log.info("Spotify OAuth 콜백 처리 - Spotify User ID: {}", spotifyUserId);
|
||||
|
||||
// 2. userId로 사용 (요청에 userId가 있으면 사용, 없으면 spotifyUserId 사용)
|
||||
String userId = request.getUserId() != null && !request.getUserId().trim().isEmpty()
|
||||
? request.getUserId()
|
||||
: spotifyUserId;
|
||||
|
||||
// 3. Redis에 토큰 저장
|
||||
spotifyTokenService.saveToken(
|
||||
userId,
|
||||
request.getAccessToken(),
|
||||
request.getRefreshToken(),
|
||||
request.getExpiresIn(),
|
||||
request.getScope()
|
||||
);
|
||||
|
||||
// 4. userId를 암호화하여 sessionId 생성
|
||||
String sessionId = encryptionUtil.encrypt(userId);
|
||||
log.info("Spotify 세션 ID 생성 완료 - userId: {}", userId);
|
||||
|
||||
return SpotifyAuthResponse.builder()
|
||||
.success(true)
|
||||
.message("인증 성공")
|
||||
.sessionId(sessionId)
|
||||
.expiresIn(request.getExpiresIn())
|
||||
.spotifyUserId(spotifyUserId)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Spotify OAuth 콜백 처리 중 오류 발생", e);
|
||||
return SpotifyAuthResponse.builder()
|
||||
.success(false)
|
||||
.message("인증 처리 중 오류가 발생했습니다: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 ID(암호화된 userId)를 사용하여 플레이리스트를 조회합니다.
|
||||
*
|
||||
* @param sessionId 암호화된 세션 ID
|
||||
* @return 플레이리스트 목록 응답
|
||||
*/
|
||||
public SpotifyPlaylistResponse getUserPlaylistsWithSession(String sessionId) {
|
||||
try {
|
||||
// 1. sessionId 복호화
|
||||
String userId;
|
||||
try {
|
||||
userId = encryptionUtil.decrypt(sessionId);
|
||||
log.info("세션 ID 복호화 성공 - userId: {}", userId);
|
||||
} catch (Exception e) {
|
||||
log.error("세션 ID 복호화 실패", e);
|
||||
return SpotifyPlaylistResponse.builder()
|
||||
.success(false)
|
||||
.message("유효하지 않은 세션 ID입니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 2. Redis에서 Access Token 조회
|
||||
String accessToken = spotifyTokenService.getAccessToken(userId);
|
||||
if (accessToken == null) {
|
||||
log.warn("Access Token을 찾을 수 없음 - userId: {}", userId);
|
||||
return SpotifyPlaylistResponse.builder()
|
||||
.success(false)
|
||||
.message("세션이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 3. 플레이리스트 조회
|
||||
return getUserPlaylists(accessToken);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Spotify 플레이리스트 조회 중 오류 발생", e);
|
||||
return SpotifyPlaylistResponse.builder()
|
||||
.success(false)
|
||||
.message("플레이리스트 조회 중 오류가 발생했습니다: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 ID를 사용하여 트랙을 플레이리스트에 추가합니다.
|
||||
*
|
||||
* @param sessionId 암호화된 세션 ID
|
||||
* @param request 트랙 추가 요청
|
||||
* @return 트랙 추가 응답
|
||||
*/
|
||||
public SpotifyAddTracksResponse addTracksToPlaylistWithSession(String sessionId, SpotifyAddTracksRequest request) {
|
||||
try {
|
||||
// 1. sessionId 복호화
|
||||
String userId;
|
||||
try {
|
||||
userId = encryptionUtil.decrypt(sessionId);
|
||||
log.info("세션 ID 복호화 성공 - userId: {}", userId);
|
||||
} catch (Exception e) {
|
||||
log.error("세션 ID 복호화 실패", e);
|
||||
return SpotifyAddTracksResponse.builder()
|
||||
.success(false)
|
||||
.message("유효하지 않은 세션 ID입니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 2. Redis에서 Access Token 조회
|
||||
String accessToken = spotifyTokenService.getAccessToken(userId);
|
||||
if (accessToken == null) {
|
||||
log.warn("Access Token을 찾을 수 없음 - userId: {}", userId);
|
||||
return SpotifyAddTracksResponse.builder()
|
||||
.success(false)
|
||||
.message("세션이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 3. request에 accessToken 설정
|
||||
request.setAccessToken(accessToken);
|
||||
|
||||
// 4. 트랙 추가
|
||||
return addTracksToPlaylist(request);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Spotify 트랙 추가 중 오류 발생", e);
|
||||
return SpotifyAddTracksResponse.builder()
|
||||
.success(false)
|
||||
.message("트랙 추가 중 오류가 발생했습니다: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 ID를 사용하여 Vibe 트랙을 Spotify에서 검색합니다.
|
||||
*
|
||||
* @param sessionId 암호화된 세션 ID
|
||||
* @param request Vibe 트랙 정보 리스트
|
||||
* @return Spotify 검색 결과
|
||||
*/
|
||||
public VibeToSpotifySearchResponse searchTracksFromVibeWithSession(String sessionId, VibeToSpotifySearchRequest request) {
|
||||
try {
|
||||
// 1. sessionId 복호화
|
||||
String userId;
|
||||
try {
|
||||
userId = encryptionUtil.decrypt(sessionId);
|
||||
log.info("세션 ID 복호화 성공 - userId: {}", userId);
|
||||
} catch (Exception e) {
|
||||
log.error("세션 ID 복호화 실패", e);
|
||||
return VibeToSpotifySearchResponse.builder()
|
||||
.success(false)
|
||||
.message("유효하지 않은 세션 ID입니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 2. Redis에서 Access Token 조회
|
||||
String accessToken = spotifyTokenService.getAccessToken(userId);
|
||||
if (accessToken == null) {
|
||||
log.warn("Access Token을 찾을 수 없음 - userId: {}", userId);
|
||||
return VibeToSpotifySearchResponse.builder()
|
||||
.success(false)
|
||||
.message("세션이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 3. request에 accessToken 설정
|
||||
request.setAccessToken(accessToken);
|
||||
|
||||
// 4. 검색 실행
|
||||
return searchTracksFromVibe(request);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Vibe 트랙 Spotify 검색 중 오류 발생", e);
|
||||
return VibeToSpotifySearchResponse.builder()
|
||||
.success(false)
|
||||
.message("검색 중 오류가 발생했습니다: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 플레이리스트에 트랙을 추가합니다.
|
||||
* Spotify API 제한으로 인해 100개씩 나눠서 추가합니다.
|
||||
*
|
||||
* @param accessToken Spotify Access Token
|
||||
* @param playlistId 플레이리스트 ID
|
||||
@@ -375,6 +571,40 @@ public class SpotifyService {
|
||||
* @return 추가된 트랙 개수
|
||||
*/
|
||||
private int addTracksToExistingPlaylist(String accessToken, String playlistId, List<String> trackUris) {
|
||||
try {
|
||||
final int BATCH_SIZE = 100; // Spotify API 제한: 한 번에 최대 100개
|
||||
int totalAdded = 0;
|
||||
|
||||
// trackUris를 100개씩 나눠서 처리
|
||||
for (int i = 0; i < trackUris.size(); i += BATCH_SIZE) {
|
||||
int endIndex = Math.min(i + BATCH_SIZE, trackUris.size());
|
||||
List<String> batchUris = trackUris.subList(i, endIndex);
|
||||
|
||||
log.info("트랙 추가 중... ({}/{}) - 현재 배치: {} 개",
|
||||
endIndex, trackUris.size(), batchUris.size());
|
||||
|
||||
int addedInBatch = addTracksBatch(accessToken, playlistId, batchUris);
|
||||
totalAdded += addedInBatch;
|
||||
}
|
||||
|
||||
log.info("트랙 추가 완료 - 총 {} 개", totalAdded);
|
||||
return totalAdded;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("트랙 추가 중 오류 발생", e);
|
||||
throw new RuntimeException("트랙 추가 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 단위로 트랙을 플레이리스트에 추가합니다.
|
||||
*
|
||||
* @param accessToken Spotify Access Token
|
||||
* @param playlistId 플레이리스트 ID
|
||||
* @param trackUris 트랙 URI 목록 (최대 100개)
|
||||
* @return 추가된 트랙 개수
|
||||
*/
|
||||
private int addTracksBatch(String accessToken, String playlistId, List<String> trackUris) {
|
||||
try {
|
||||
String addTracksUrl = String.format(SPOTIFY_ADD_TRACKS_URL, playlistId);
|
||||
|
||||
@@ -402,21 +632,21 @@ public class SpotifyService {
|
||||
int statusCode = response.getCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
|
||||
log.info("Spotify Add Tracks API 응답 코드: {}", statusCode);
|
||||
log.debug("Spotify Add Tracks API 응답 코드: {}", statusCode);
|
||||
|
||||
if (statusCode == 201) {
|
||||
log.info("트랙 추가 성공 - {} 개", trackUris.size());
|
||||
log.debug("배치 트랙 추가 성공 - {} 개", trackUris.size());
|
||||
return trackUris.size();
|
||||
} else {
|
||||
log.error("트랙 추가 실패. 상태 코드: {}, 응답: {}", statusCode, responseBody);
|
||||
log.error("배치 트랙 추가 실패. 상태 코드: {}, 응답: {}", statusCode, responseBody);
|
||||
throw new RuntimeException("트랙 추가 실패: " + responseBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("트랙 추가 중 오류 발생", e);
|
||||
throw new RuntimeException("트랙 추가 실패", e);
|
||||
log.error("배치 트랙 추가 중 오류 발생", e);
|
||||
throw new RuntimeException("배치 트랙 추가 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
135
src/main/java/com/domain/service/SpotifyTokenService.java
Normal file
135
src/main/java/com/domain/service/SpotifyTokenService.java
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.domain.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SpotifyTokenService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String SPOTIFY_TOKEN_PREFIX = "spotify:token:";
|
||||
|
||||
/**
|
||||
* userId를 키로 Spotify 토큰 정보를 Redis에 저장합니다.
|
||||
*
|
||||
* @param userId 사용자 아이디
|
||||
* @param accessToken Access Token
|
||||
* @param refreshToken Refresh Token
|
||||
* @param expiresIn 만료 시간 (초)
|
||||
* @param scope 권한 범위
|
||||
*/
|
||||
public void saveToken(String userId, String accessToken, String refreshToken, Integer expiresIn, String scope) {
|
||||
String key = SPOTIFY_TOKEN_PREFIX + userId;
|
||||
|
||||
Map<String, String> tokenData = new HashMap<>();
|
||||
tokenData.put("access_token", accessToken);
|
||||
tokenData.put("refresh_token", refreshToken);
|
||||
tokenData.put("scope", scope);
|
||||
tokenData.put("expires_in", String.valueOf(expiresIn));
|
||||
|
||||
redisTemplate.opsForHash().putAll(key, tokenData);
|
||||
|
||||
// 토큰 만료 시간보다 조금 짧게 설정 (안전 마진 60초)
|
||||
long ttlSeconds = expiresIn > 60 ? expiresIn - 60 : expiresIn;
|
||||
redisTemplate.expire(key, ttlSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("Spotify 토큰 저장 완료 - userId: {}, 유효기간: {}초", userId, ttlSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* userId로 Access Token을 조회합니다.
|
||||
*
|
||||
* @param userId 사용자 아이디
|
||||
* @return Access Token (없으면 null)
|
||||
*/
|
||||
public String getAccessToken(String userId) {
|
||||
String key = SPOTIFY_TOKEN_PREFIX + userId;
|
||||
|
||||
Object token = redisTemplate.opsForHash().get(key, "access_token");
|
||||
if (token == null) {
|
||||
log.info("Redis에 저장된 Spotify 토큰 없음 - userId: {}", userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("Spotify Access Token 조회 - userId: {}", userId);
|
||||
return (String) token;
|
||||
}
|
||||
|
||||
/**
|
||||
* userId로 Refresh Token을 조회합니다.
|
||||
*
|
||||
* @param userId 사용자 아이디
|
||||
* @return Refresh Token (없으면 null)
|
||||
*/
|
||||
public String getRefreshToken(String userId) {
|
||||
String key = SPOTIFY_TOKEN_PREFIX + userId;
|
||||
|
||||
Object token = redisTemplate.opsForHash().get(key, "refresh_token");
|
||||
if (token == null) {
|
||||
log.info("Redis에 저장된 Spotify Refresh Token 없음 - userId: {}", userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) token;
|
||||
}
|
||||
|
||||
/**
|
||||
* userId로 모든 토큰 정보를 조회합니다.
|
||||
*
|
||||
* @param userId 사용자 아이디
|
||||
* @return 토큰 정보 맵 (없으면 null)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> getTokenData(String userId) {
|
||||
String key = SPOTIFY_TOKEN_PREFIX + userId;
|
||||
|
||||
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
|
||||
if (entries.isEmpty()) {
|
||||
log.info("Redis에 저장된 Spotify 토큰 데이터 없음 - userId: {}", userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("Spotify 토큰 데이터 조회 - userId: {}", userId);
|
||||
return (Map<String, String>) (Map<?, ?>) entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* userId의 토큰을 삭제합니다.
|
||||
*
|
||||
* @param userId 사용자 아이디
|
||||
*/
|
||||
public void deleteToken(String userId) {
|
||||
String key = SPOTIFY_TOKEN_PREFIX + userId;
|
||||
redisTemplate.delete(key);
|
||||
log.info("Spotify 토큰 삭제 완료 - userId: {}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token만 갱신합니다.
|
||||
*
|
||||
* @param userId 사용자 아이디
|
||||
* @param accessToken 새로운 Access Token
|
||||
* @param expiresIn 만료 시간 (초)
|
||||
*/
|
||||
public void updateAccessToken(String userId, String accessToken, Integer expiresIn) {
|
||||
String key = SPOTIFY_TOKEN_PREFIX + userId;
|
||||
|
||||
redisTemplate.opsForHash().put(key, "access_token", accessToken);
|
||||
redisTemplate.opsForHash().put(key, "expires_in", String.valueOf(expiresIn));
|
||||
|
||||
// TTL 갱신
|
||||
long ttlSeconds = expiresIn > 60 ? expiresIn - 60 : expiresIn;
|
||||
redisTemplate.expire(key, ttlSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("Spotify Access Token 갱신 완료 - userId: {}, 유효기간: {}초", userId, ttlSeconds);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user