Skip to content

Commit 0eb252b

Browse files
authored
feat: youtube info 캐싱 및 플레이스토어 테스트용 api 추가
# 변경점 👍 close: #22 1. `YoutubeInfo` 엔티티를 생성해서 youtube api 호출 결과를 저장했습니다. <img width="1324" height="346" alt="스크린샷 2025-11-21 오전 11 40 54" src="https://github.com/user-attachments/assets/bff7bbac-4e0f-4a63-b9c0-01abc20b633e" /> 첫번째 호출 <img width="1318" height="309" alt="스크린샷 2025-11-21 오전 11 41 04" src="https://github.com/user-attachments/assets/60e8648d-4fe5-4be0-aa5e-75853d72725d" /> 두번째 호출 <br> <br> 2. 플레이 스토어 테스트용 api 추가 `TEST-IDENTIFIER` 라는 identifier를 가진 테스트용 계정이 없으면 생성하고 토큰을 반환합니다. 존재한다면 이미 존재하는 계정을 반환합니다.
1 parent 0610b79 commit 0eb252b

File tree

10 files changed

+242
-8
lines changed

10 files changed

+242
-8
lines changed

src/main/java/apptive/team5/oauth2/controller/OAuth2Controller.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import apptive.team5.oauth2.dto.KakaoLoginRequest;
66
import apptive.team5.oauth2.service.GoogleService;
77
import apptive.team5.oauth2.service.KakaoService;
8+
import apptive.team5.oauth2.service.TestLoginService;
89
import lombok.RequiredArgsConstructor;
910
import org.springframework.http.ResponseEntity;
1011
import org.springframework.web.bind.annotation.*;
@@ -16,6 +17,7 @@ public class OAuth2Controller {
1617

1718
private final KakaoService kakaoService;
1819
private final GoogleService googleService;
20+
private final TestLoginService testLoginService;
1921

2022
@PostMapping("/kakao")
2123
public ResponseEntity<TokenResponse> kakaoLogin(@RequestBody KakaoLoginRequest kakaoLoginRequest) {
@@ -32,4 +34,12 @@ public ResponseEntity<TokenResponse> googleLogin(@RequestBody GoogleLoginRequest
3234

3335
return ResponseEntity.ok(tokenResponse);
3436
}
37+
38+
@GetMapping("/test")
39+
public ResponseEntity<TokenResponse> testLogin() {
40+
41+
TokenResponse tokenResponse = testLoginService.testLogin();
42+
43+
return ResponseEntity.ok(tokenResponse);
44+
}
3545
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package apptive.team5.oauth2.service;
2+
3+
import apptive.team5.global.exception.NotFoundEntityException;
4+
import apptive.team5.jwt.TokenType;
5+
import apptive.team5.jwt.component.JWTUtil;
6+
import apptive.team5.jwt.dto.TokenResponse;
7+
import apptive.team5.jwt.service.JwtService;
8+
import apptive.team5.user.domain.SocialType;
9+
import apptive.team5.user.domain.UserEntity;
10+
import apptive.team5.user.domain.UserRoleType;
11+
import apptive.team5.user.repository.UserRepository;
12+
import apptive.team5.user.service.UserLowService;
13+
import apptive.team5.user.service.UserService;
14+
import jakarta.persistence.EntityManager;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
19+
import java.util.Optional;
20+
21+
@Transactional
22+
@Service
23+
@RequiredArgsConstructor
24+
public class TestLoginService {
25+
26+
private final UserRepository userRepository;
27+
private static final String TEST_IDENTIFIER = "TEST-IDENTIFIER";
28+
private final JWTUtil jwtUtil;
29+
private final JwtService jwtService;
30+
31+
public TokenResponse testLogin() {
32+
33+
UserEntity user;
34+
35+
Optional<UserEntity> findUser = userRepository.findByIdentifier(TEST_IDENTIFIER);
36+
37+
if(findUser.isPresent()){
38+
user = findUser.get();
39+
}
40+
else user = userRepository.save(new UserEntity(TEST_IDENTIFIER, "[email protected]", "tester", "tester", UserRoleType.USER, SocialType.KAKAO));
41+
42+
43+
44+
String accessToken = jwtUtil.createJWT(user.getId(), "ROLE_" + user.getRoleType().name(), TokenType.ACCESS_TOKEN);
45+
String refreshToken = jwtUtil.createJWT(user.getId(), "ROLE_" + user.getRoleType().name(), TokenType.REFRESH_TOKEN);
46+
47+
jwtService.saveRefreshToken(user.getId(), refreshToken);
48+
49+
return new TokenResponse(accessToken, refreshToken);
50+
}
51+
}

src/main/java/apptive/team5/youtube/controller/YoutubeApiController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ public class YoutubeApiController {
2121
private final YoutubeService youtubeService;
2222

2323
@GetMapping
24-
public ResponseEntity<List<YoutubeVideoResponse>> searchVideo(@RequestParam String artist, @RequestParam String title) {
24+
public ResponseEntity<List<YoutubeVideoResponse>> searchVideo(@RequestParam String id, @RequestParam String artist,
25+
@RequestParam String title) {
2526

26-
List<YoutubeVideoResponse> response = youtubeService.searchVideo(new YoutubeSearchRequest(artist, title));
27+
List<YoutubeVideoResponse> response = youtubeService.searchVideo(new YoutubeSearchRequest(id, artist, title));
2728

2829
return ResponseEntity.status(HttpStatus.OK).body(response);
2930
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package apptive.team5.youtube.domain;
2+
3+
import apptive.team5.youtube.dto.YoutubeVideoResponse;
4+
import jakarta.persistence.*;
5+
import lombok.AccessLevel;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Entity
10+
@Getter
11+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
12+
public class YoutubeInfo {
13+
14+
@Id
15+
@GeneratedValue(strategy = GenerationType.IDENTITY)
16+
private Long id;
17+
18+
@Column(nullable = false)
19+
private String spotifyId;
20+
21+
@Column(nullable = false)
22+
private String title;
23+
24+
@Column(nullable = false)
25+
private String duration;
26+
27+
@Column(nullable = false)
28+
private String url;
29+
30+
31+
public YoutubeInfo(String spotifyId, YoutubeVideoResponse youtubeVideoResponse) {
32+
this.spotifyId = spotifyId;
33+
this.title = youtubeVideoResponse.title();
34+
this.duration = youtubeVideoResponse.duration();
35+
this.url = youtubeVideoResponse.url();
36+
}
37+
38+
}

src/main/java/apptive/team5/youtube/dto/YoutubeSearchRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package apptive.team5.youtube.dto;
22

33
public record YoutubeSearchRequest(
4+
String spotifyId,
45
String artist,
56
String title
67
) {

src/main/java/apptive/team5/youtube/dto/YoutubeVideoResponse.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package apptive.team5.youtube.dto;
22

3+
import apptive.team5.youtube.domain.YoutubeInfo;
34
import com.google.api.services.youtube.model.Video;
45

56
public record YoutubeVideoResponse (
@@ -13,6 +14,10 @@ public YoutubeVideoResponse(Video video) {
1314
"https://www.youtube-nocookie.com/embed/" + video.getId());
1415
}
1516

17+
public YoutubeVideoResponse(YoutubeInfo youtubeInfo) {
18+
this(youtubeInfo.getTitle(), youtubeInfo.getDuration(), youtubeInfo.getUrl());
19+
}
20+
1621
@Override
1722
public int compareTo(YoutubeVideoResponse other) {
1823
return Integer.compare(priority(this.title), priority(other.title));
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package apptive.team5.youtube.repository;
2+
3+
import apptive.team5.youtube.domain.YoutubeInfo;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.List;
7+
8+
public interface YoutubeInfoRepository extends JpaRepository<YoutubeInfo, Long> {
9+
List<YoutubeInfo> findBySpotifyId(String spotifyId);
10+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package apptive.team5.youtube.service;
2+
3+
import apptive.team5.youtube.domain.YoutubeInfo;
4+
import apptive.team5.youtube.repository.YoutubeInfoRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
@Service
13+
@Transactional
14+
@RequiredArgsConstructor
15+
public class YoutubeInfoLowService {
16+
17+
private final YoutubeInfoRepository youtubeInfoRepository;
18+
19+
20+
public List<YoutubeInfo> saveAll(List<YoutubeInfo> youtubeInfos) {
21+
return youtubeInfoRepository.saveAll(youtubeInfos);
22+
}
23+
24+
@Transactional(readOnly = true)
25+
public List<YoutubeInfo> findBySpotifyId(String spotifyId) {
26+
return youtubeInfoRepository.findBySpotifyId(spotifyId);
27+
}
28+
29+
30+
}

src/main/java/apptive/team5/youtube/service/YoutubeService.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,45 @@
33
import apptive.team5.global.exception.ExceptionCode;
44
import apptive.team5.global.exception.ExternalApiConnectException;
55
import apptive.team5.youtube.YoutubeApiKeyProvider;
6+
import apptive.team5.youtube.domain.YoutubeInfo;
67
import apptive.team5.youtube.dto.YoutubeSearchRequest;
78
import apptive.team5.youtube.dto.YoutubeVideoResponse;
89
import com.google.api.client.http.javanet.NetHttpTransport;
910
import com.google.api.client.json.gson.GsonFactory;
1011
import com.google.api.services.youtube.YouTube;
1112
import com.google.api.services.youtube.model.SearchListResponse;
1213
import com.google.api.services.youtube.model.VideoListResponse;
14+
import lombok.RequiredArgsConstructor;
1315
import org.springframework.http.HttpStatus;
1416
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
1518

1619
import java.io.IOException;
1720
import java.util.Collections;
1821
import java.util.List;
1922
import java.util.Objects;
23+
import java.util.Optional;
2024

25+
@Transactional
2126
@Service
27+
@RequiredArgsConstructor
2228
public class YoutubeService {
2329

2430
private final YoutubeApiKeyProvider apiKeyProvider;
2531
private static final GsonFactory gsonFactory = new GsonFactory();
26-
27-
public YoutubeService(YoutubeApiKeyProvider provider) {
28-
this.apiKeyProvider = provider;
29-
}
32+
private final YoutubeInfoLowService youtubeInfoLowService;
3033

3134
public List<YoutubeVideoResponse> searchVideo(YoutubeSearchRequest searchRequest) {
3235

36+
List<YoutubeInfo> findYoutubeInfo = youtubeInfoLowService.findBySpotifyId(searchRequest.spotifyId());
37+
38+
if (!findYoutubeInfo.isEmpty()) {
39+
return findYoutubeInfo.stream()
40+
.map(YoutubeVideoResponse::new)
41+
.sorted()
42+
.toList();
43+
}
44+
3345
String apiKey = apiKeyProvider.nextKey();
3446

3547
YouTube youtube = new YouTube.Builder(
@@ -51,18 +63,29 @@ public List<YoutubeVideoResponse> searchVideo(YoutubeSearchRequest searchRequest
5163
.filter(Objects::nonNull)
5264
.toList();
5365

54-
VideoListResponse videoResponse = youtube.videos()
66+
VideoListResponse videoListResponse = youtube.videos()
5567
.list(Collections.singletonList("snippet,contentDetails,statistics"))
5668
.setId(Collections.singletonList(String.join(",", videoIds)))
5769
.setKey(apiKey)
5870
.execute();
5971

60-
return videoResponse.getItems()
72+
if(videoListResponse.isEmpty()) return List.of();
73+
74+
List<YoutubeVideoResponse> youtubeVideoResponses = videoListResponse.getItems()
6175
.stream()
6276
.map(YoutubeVideoResponse::new)
6377
.sorted()
6478
.toList();
6579

80+
List<YoutubeInfo> youtubeInfos = youtubeVideoResponses.
81+
stream()
82+
.map(videoResponse -> new YoutubeInfo(searchRequest.spotifyId(), videoResponse))
83+
.toList();
84+
85+
youtubeInfoLowService.saveAll(youtubeInfos);
86+
87+
return youtubeVideoResponses;
88+
6689
} catch (IOException e) {
6790
throw new ExternalApiConnectException(
6891
ExceptionCode.YOUTUBE_API_EXCEPTION.getDescription(),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package apptive.team5.youtube.service;
2+
3+
import apptive.team5.youtube.YoutubeApiKeyProvider;
4+
import apptive.team5.youtube.domain.YoutubeInfo;
5+
import apptive.team5.youtube.dto.YoutubeSearchRequest;
6+
import apptive.team5.youtube.dto.YoutubeVideoResponse;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.ExtendWith;
10+
import org.mockito.InjectMocks;
11+
import org.mockito.Mock;
12+
import org.springframework.test.context.junit.jupiter.SpringExtension;
13+
14+
import java.util.List;
15+
import java.util.Optional;
16+
17+
import static org.assertj.core.api.SoftAssertions.*;
18+
import static org.mockito.ArgumentMatchers.any;
19+
import static org.mockito.BDDMockito.given;
20+
import static org.mockito.Mockito.verify;
21+
import static org.mockito.Mockito.verifyNoMoreInteractions;
22+
23+
@ExtendWith(SpringExtension.class)
24+
class YoutubeServiceTest {
25+
26+
@InjectMocks
27+
private YoutubeService youtubeService;
28+
29+
@Mock
30+
private YoutubeInfoLowService youtubeInfoLowService;;
31+
32+
@Mock
33+
private YoutubeApiKeyProvider youtubeApiKeyProvider;
34+
35+
36+
@Test
37+
@DisplayName("이미 존재하는 YoutubeInfo면 youtube api 호출 없이 바로 반환")
38+
void getYoutubeVideoResponseWithSpotifyId() {
39+
40+
// given
41+
YoutubeSearchRequest youtubeSearchRequest = new YoutubeSearchRequest("1234", "test", "test");
42+
YoutubeVideoResponse youtubeVideoResponse = new YoutubeVideoResponse("test", "duration", "url");
43+
44+
YoutubeInfo youtubeInfo = new YoutubeInfo(youtubeSearchRequest.spotifyId(), youtubeVideoResponse);
45+
46+
given(youtubeInfoLowService.findBySpotifyId(any()))
47+
.willReturn(List.of(youtubeInfo));
48+
49+
// when
50+
List<YoutubeVideoResponse> youtubeVideoResponses = youtubeService.searchVideo(youtubeSearchRequest);
51+
52+
// then
53+
YoutubeVideoResponse result = youtubeVideoResponses.getFirst();
54+
assertSoftly(softly -> {
55+
softly.assertThat(result.title()).isEqualTo(youtubeVideoResponse.title());
56+
softly.assertThat(result.duration()).isEqualTo(youtubeVideoResponse.duration());
57+
softly.assertThat(result.url()).isEqualTo(youtubeVideoResponse.url());
58+
});
59+
60+
verify(youtubeInfoLowService).findBySpotifyId(any());
61+
verifyNoMoreInteractions(youtubeInfoLowService, youtubeApiKeyProvider);
62+
63+
}
64+
65+
}

0 commit comments

Comments
 (0)