diff --git a/build.gradle b/build.gradle index 4084531..69f9d7a 100644 --- a/build.gradle +++ b/build.gradle @@ -22,8 +22,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -39,6 +39,12 @@ dependencies { //s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + //youtube api + implementation 'com.google.api-client:google-api-client:1.33.0' + implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0' + implementation 'com.google.apis:google-api-services-youtube:v3-rev20230816-2.0.0' + implementation 'com.google.http-client:google-http-client-jackson2:1.39.2' } tasks.named('test') { diff --git a/src/main/java/com/example/beginnerfitbe/BeginnerFitBeApplication.java b/src/main/java/com/example/beginnerfitbe/BeginnerFitBeApplication.java index 93f4541..4266e56 100644 --- a/src/main/java/com/example/beginnerfitbe/BeginnerFitBeApplication.java +++ b/src/main/java/com/example/beginnerfitbe/BeginnerFitBeApplication.java @@ -2,12 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class BeginnerFitBeApplication { - public static void main(String[] args) { SpringApplication.run(BeginnerFitBeApplication.class, args); } - } diff --git a/src/main/java/com/example/beginnerfitbe/category/service/CategoryService.java b/src/main/java/com/example/beginnerfitbe/category/service/CategoryService.java index 73ecfa3..eb407b6 100644 --- a/src/main/java/com/example/beginnerfitbe/category/service/CategoryService.java +++ b/src/main/java/com/example/beginnerfitbe/category/service/CategoryService.java @@ -2,7 +2,6 @@ import com.example.beginnerfitbe.category.domain.Category; import com.example.beginnerfitbe.category.repository.CategoryRepository; -import com.example.beginnerfitbe.post.domain.Post; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/beginnerfitbe/jwt/config/SecurityConfig.java b/src/main/java/com/example/beginnerfitbe/jwt/config/SecurityConfig.java index 9f27929..2dd68f6 100644 --- a/src/main/java/com/example/beginnerfitbe/jwt/config/SecurityConfig.java +++ b/src/main/java/com/example/beginnerfitbe/jwt/config/SecurityConfig.java @@ -27,8 +27,6 @@ public class SecurityConfig { "/auth/sign-in", "/auth/sign-up", "/users/emailcheck", - "/video", - "/playlist", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", diff --git a/src/main/java/com/example/beginnerfitbe/playlist/controller/PlaylistController.java b/src/main/java/com/example/beginnerfitbe/playlist/controller/PlaylistController.java new file mode 100644 index 0000000..b9032e9 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/controller/PlaylistController.java @@ -0,0 +1,45 @@ +package com.example.beginnerfitbe.playlist.controller; + +import com.example.beginnerfitbe.jwt.util.JwtUtil; +import com.example.beginnerfitbe.playlist.service.PlaylistService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/playlists") +@RequiredArgsConstructor +public class PlaylistController { + + private final JwtUtil jwtUtil; + private final PlaylistService playlistService; + + @GetMapping("") + @Operation(summary = "플레이리스트 전체 조회 메소드", description = "전체 플레이리스트를 조회합니다.") + public ResponseEntity list(){ + return ResponseEntity.ok(playlistService.list()); + } + + @GetMapping("/{playlistId}") + @Operation(summary = "플레이리스트 상세 조회 메소드", description = "플레이리스트 상세 정보를 조회합니다.") + public ResponseEntity read(@PathVariable Long playlistId){ + return ResponseEntity.ok(playlistService.read(playlistId)); + } + + @GetMapping("/me") + @Operation(summary = "사용자의 플레이리스트 목록을 조회합니다.", description = "사용자의 플레이리스트 목록을 조회합니다.") + public ResponseEntity me(HttpServletRequest request){ + Long userId = jwtUtil.getUserId(jwtUtil.resolveToken(request).substring(7)); + return ResponseEntity.ok(playlistService.me(userId)); + } + + @GetMapping("/recent") + @Operation(summary = "홈화면에 뜰 플레이리스트를 조회합니다.", description = "가장 최신 플레이리스트를 조회합니다.") + public ResponseEntity getRecentPlaylist(HttpServletRequest request){ + Long userId = jwtUtil.getUserId(jwtUtil.resolveToken(request).substring(7)); + return ResponseEntity.ok(playlistService.getRecentPlaylist(userId)); + } +} diff --git a/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java new file mode 100644 index 0000000..037668d --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java @@ -0,0 +1,55 @@ +package com.example.beginnerfitbe.playlist.domain; + +import com.example.beginnerfitbe.user.domain.User; +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@NoArgsConstructor +@Getter +public class Playlist { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String totalTime; + + @Column(nullable = false) + private Boolean isCompleted; + + @CreatedDate + @Column(nullable = false) + private LocalDateTime createdAt; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToMany(mappedBy = "playlist", fetch = FetchType.LAZY) + private List videos; + + @Builder + public Playlist(String title, String totalTime, Boolean isCompleted, LocalDateTime createdAt, User user, List videos) { + this.title = title; + this.totalTime = totalTime; + this.isCompleted = isCompleted; + this.createdAt = createdAt; + this.user = user; + this.videos = videos; + } + + public void setCompleted(boolean isCompleted) { + this.isCompleted = isCompleted; + } +} diff --git a/src/main/java/com/example/beginnerfitbe/playlist/dto/PlaylistCreateDto.java b/src/main/java/com/example/beginnerfitbe/playlist/dto/PlaylistCreateDto.java new file mode 100644 index 0000000..6ab5fd9 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/dto/PlaylistCreateDto.java @@ -0,0 +1,33 @@ +package com.example.beginnerfitbe.playlist.dto; + +import com.example.beginnerfitbe.user.domain.User; +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +public class PlaylistCreateDto { + private String title; + private String description; + private int totalTime; + private Boolean isWatched; + private LocalDateTime createdAt; + private User user; + private List youtubeVideos; + + @Builder + public PlaylistCreateDto(String title, String description, int totalTime, Boolean isWatched, LocalDateTime createdAt, User user, List youtubeVideos) { + this.title = title; + this.description = description; + this.totalTime = totalTime; + this.isWatched = isWatched; + this.createdAt = createdAt; + this.user = user; + this.youtubeVideos = youtubeVideos; + } +} diff --git a/src/main/java/com/example/beginnerfitbe/playlist/dto/PlaylistDto.java b/src/main/java/com/example/beginnerfitbe/playlist/dto/PlaylistDto.java new file mode 100644 index 0000000..a84b155 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/dto/PlaylistDto.java @@ -0,0 +1,48 @@ +package com.example.beginnerfitbe.playlist.dto; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import com.example.beginnerfitbe.youtube.dto.YoutubeVideoDto; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor +public class PlaylistDto { + private Long id; + private String title; + private String totalTime; + private Boolean isCompleted; + private LocalDateTime createdAt; + private Long userId; + private List videos; + + @Builder + public PlaylistDto(Long id, String title, String totalTime, Boolean isCompleted, LocalDateTime createdAt, Long userId, List videos) { + this.id = id; + this.title = title; + this.totalTime = totalTime; + this.isCompleted = isCompleted; + this.createdAt = createdAt; + this.userId = userId; + this.videos = videos; + } + + public static PlaylistDto fromEntity(Playlist playlist) { + return PlaylistDto.builder() + .id(playlist.getId()) + .title(playlist.getTitle()) + .totalTime(playlist.getTotalTime()) + .isCompleted(playlist.getIsCompleted()) + .createdAt(playlist.getCreatedAt()) + .userId(playlist.getUser().getId()) + .videos(playlist.getVideos().stream() + .map(YoutubeVideoDto::fromEntity) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/com/example/beginnerfitbe/playlist/repository/PlaylistRepository.java b/src/main/java/com/example/beginnerfitbe/playlist/repository/PlaylistRepository.java new file mode 100644 index 0000000..53a37c4 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/repository/PlaylistRepository.java @@ -0,0 +1,15 @@ +package com.example.beginnerfitbe.playlist.repository; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import com.example.beginnerfitbe.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PlaylistRepository extends JpaRepository { + List findByUserOrderByCreatedAtDesc(User user); + Optional findFirstByUserOrderByCreatedAtDesc(User user); +} diff --git a/src/main/java/com/example/beginnerfitbe/playlist/service/PlaylistService.java b/src/main/java/com/example/beginnerfitbe/playlist/service/PlaylistService.java new file mode 100644 index 0000000..15aadde --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/service/PlaylistService.java @@ -0,0 +1,125 @@ +package com.example.beginnerfitbe.playlist.service; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import com.example.beginnerfitbe.playlist.dto.PlaylistDto; +import com.example.beginnerfitbe.playlist.repository.PlaylistRepository; +import com.example.beginnerfitbe.youtube.dto.SelectedVideoDto; +import com.example.beginnerfitbe.user.domain.User; +import com.example.beginnerfitbe.user.repository.UserRepository; +import com.example.beginnerfitbe.youtube.dto.YoutubeVideoDto; +import com.example.beginnerfitbe.youtube.service.YoutubeVideoService; +import com.example.beginnerfitbe.youtube.util.YoutubeUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PlaylistService { + + private static final Logger logger = LoggerFactory.getLogger(PlaylistService.class); + + private final PlaylistRepository playlistRepository; + private final UserRepository userRepository; + private final YoutubeVideoService youtubeVideoService; + private final YoutubeUtil youtubeUtil; + + public PlaylistDto create(User user) throws IOException { + String query = searchKeyword(user); + String requestTime = String.valueOf(user.getExerciseTime()); + SelectedVideoDto selectVideoDto = youtubeUtil.selectVideos(query, requestTime); + + Playlist playlist = Playlist.builder() + .title(query + " 집중 공략하기 플리") + .createdAt(LocalDateTime.now()) + .isCompleted(false) + .totalTime(selectVideoDto.getTotalTime()) + .user(user) + .videos(selectVideoDto.getYoutubeVideos()) + .build(); + + playlist = playlistRepository.save(playlist); + + youtubeVideoService.create(selectVideoDto, playlist); + return PlaylistDto.fromEntity(playlist); + } + + + // 매일 자정에 실행 + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + public void createPlaylistDaily() { + userRepository.findAll().forEach(user -> { + try { + create(user); + } catch (IOException e) { + logger.error("플레이리스트 생성 중 오류 발생", e); + } + }); + } + + private String searchKeyword(User user) { + + String query= null; + if(user.getExerciseIntensity()<=3){ + query = user.getExercisePart() + " " + user.getExercisePurpose() +" 쉬운 운동"; + } + else if(user.getExerciseIntensity()>=7){ + query = user.getExercisePart() + " " + user.getExercisePurpose() +" 매운맛 운동"; + } + else{ + query = user.getExercisePart() + " " + user.getExercisePurpose()+" 운동"; + } + return query; + } + + @Transactional + public void update(Long playlistId) { + List samePlaylistVideos = youtubeVideoService.getYoutubeVideosByPlaylist(playlistId); // 비디오 상태 확인 + + boolean allWatched = samePlaylistVideos.stream().allMatch(YoutubeVideoDto::getIsWatched); + + if (allWatched) { + Playlist playlist = playlistRepository.findById(playlistId) + .orElseThrow(() -> new IllegalArgumentException("Playlist not found")); + playlist.setCompleted(true); + playlistRepository.save(playlist); + } + } + //재생목록 조회 + public List list(){ + return playlistRepository.findAll().stream() + .map(PlaylistDto::fromEntity) + .collect(Collectors.toList()); + } + + public PlaylistDto read(Long playlistId){ + Playlist playlist = playlistRepository.findById(playlistId).orElseThrow(() -> new IllegalArgumentException("Playlist not found")); + return PlaylistDto.fromEntity(playlist); + } + + //사용자 플레이리스트 목록 + public List me(Long userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found")); + return playlistRepository.findByUserOrderByCreatedAtDesc(user).stream() + .map(PlaylistDto::fromEntity) + .collect(Collectors.toList()); + } + + //가장 최신 플레이리스트 (홈화면에 뜰 플레이리스트 ) + public PlaylistDto getRecentPlaylist(Long userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found")); + Playlist playlist = playlistRepository.findFirstByUserOrderByCreatedAtDesc(user).orElseThrow(() -> new IllegalArgumentException("Playlist not found")); + return PlaylistDto.fromEntity(playlist); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/beginnerfitbe/user/controller/AuthController.java b/src/main/java/com/example/beginnerfitbe/user/controller/AuthController.java index 995350e..61b9aec 100644 --- a/src/main/java/com/example/beginnerfitbe/user/controller/AuthController.java +++ b/src/main/java/com/example/beginnerfitbe/user/controller/AuthController.java @@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; + @RestController @RequiredArgsConstructor @RequestMapping("/auth") @@ -19,7 +21,7 @@ public class AuthController { private final UserService userService; @PostMapping("/sign-up") - public ResponseEntity signUp(@RequestBody SignUpReqDto dto) { + public ResponseEntity signUp(@RequestBody SignUpReqDto dto) throws IOException { userService.create(authService.signUp(dto)); return ResponseEntity.created(null).build(); } diff --git a/src/main/java/com/example/beginnerfitbe/user/domain/User.java b/src/main/java/com/example/beginnerfitbe/user/domain/User.java index 373b1ea..b465268 100644 --- a/src/main/java/com/example/beginnerfitbe/user/domain/User.java +++ b/src/main/java/com/example/beginnerfitbe/user/domain/User.java @@ -27,10 +27,10 @@ public class User { private String profilePictureUrl; @Column - private int exercisePurpose; + private String exercisePurpose; @Column - private int exercisePart; + private String exercisePart; @Column private int exerciseTime; @@ -39,7 +39,7 @@ public class User { private int exerciseIntensity; @Builder - public User(String email, String name, String password, String profilePictureUrl, int exercisePurpose, int exercisePart, int exerciseTime, int exerciseIntensity) { + public User(String email, String name, String password, String profilePictureUrl, String exercisePurpose, String exercisePart, int exerciseTime, int exerciseIntensity) { this.email=email; this.name = name; this.password = password; @@ -50,7 +50,7 @@ public User(String email, String name, String password, String profilePictureUrl this.exerciseIntensity = exerciseIntensity; } - public void update(String name, int exercisePurpose, int exercisePart, int exerciseTime, int exerciseIntensity) { + public void update(String name, String exercisePurpose, String exercisePart, int exerciseTime, int exerciseIntensity) { this.name=name; this.exercisePurpose = exercisePurpose; this.exercisePart= exercisePart; diff --git a/src/main/java/com/example/beginnerfitbe/user/dto/SignInResDto.java b/src/main/java/com/example/beginnerfitbe/user/dto/SignInResDto.java index 487f3ed..2d0a5a6 100644 --- a/src/main/java/com/example/beginnerfitbe/user/dto/SignInResDto.java +++ b/src/main/java/com/example/beginnerfitbe/user/dto/SignInResDto.java @@ -7,6 +7,6 @@ @AllArgsConstructor @Getter public class SignInResDto { - private String accssToken; + private String accessToken; } diff --git a/src/main/java/com/example/beginnerfitbe/user/dto/SignUpReqDto.java b/src/main/java/com/example/beginnerfitbe/user/dto/SignUpReqDto.java index eeade84..4c1e85a 100644 --- a/src/main/java/com/example/beginnerfitbe/user/dto/SignUpReqDto.java +++ b/src/main/java/com/example/beginnerfitbe/user/dto/SignUpReqDto.java @@ -10,8 +10,8 @@ public class SignUpReqDto { private String email; private String name; private String password; - private int exercisePurpose; - private int exercisePart; + private String exercisePurpose; + private String exercisePart; private int exerciseTime; private int exerciseIntensity; diff --git a/src/main/java/com/example/beginnerfitbe/user/dto/UserDto.java b/src/main/java/com/example/beginnerfitbe/user/dto/UserDto.java index b759e3c..1965667 100644 --- a/src/main/java/com/example/beginnerfitbe/user/dto/UserDto.java +++ b/src/main/java/com/example/beginnerfitbe/user/dto/UserDto.java @@ -10,12 +10,12 @@ public class UserDto { private String name; private String password; private String profilePictureUrl; - private int exercisePurpose; - private int exercisePart; + private String exercisePurpose; + private String exercisePart; private int exerciseTime; private int exerciseIntensity; - public UserDto(Long id, String email, String name, String password, String profilePictureUrl, int exercisePurpose, int exercisePart, int exerciseTime, int exerciseIntensity) { + public UserDto(Long id, String email, String name, String password, String profilePictureUrl, String exercisePurpose, String exercisePart, int exerciseTime, int exerciseIntensity) { this.id = id; this.email = email; this.name = name; diff --git a/src/main/java/com/example/beginnerfitbe/user/dto/UserUpdateDto.java b/src/main/java/com/example/beginnerfitbe/user/dto/UserUpdateDto.java index e001169..6d0cc71 100644 --- a/src/main/java/com/example/beginnerfitbe/user/dto/UserUpdateDto.java +++ b/src/main/java/com/example/beginnerfitbe/user/dto/UserUpdateDto.java @@ -6,8 +6,8 @@ @Data public class UserUpdateDto { private String name; - private int exercisePurpose; - private int exercisePart; + private String exercisePurpose; + private String exercisePart; private int exerciseTime; private int exerciseIntensity; diff --git a/src/main/java/com/example/beginnerfitbe/user/service/AuthService.java b/src/main/java/com/example/beginnerfitbe/user/service/AuthService.java index 3501616..8279614 100644 --- a/src/main/java/com/example/beginnerfitbe/user/service/AuthService.java +++ b/src/main/java/com/example/beginnerfitbe/user/service/AuthService.java @@ -21,8 +21,8 @@ public User signUp (SignUpReqDto dto){ String email=dto.getEmail(); String name=dto.getName(); String password=passwordEncoder.encode(dto.getPassword()); - int exercisePurpose = dto.getExercisePurpose(); - int exercisePart =dto.getExercisePart(); + String exercisePurpose = dto.getExercisePurpose(); + String exercisePart =dto.getExercisePart(); int exerciseTime = dto.getExerciseTime(); int exerciseIntensity = dto.getExerciseIntensity(); diff --git a/src/main/java/com/example/beginnerfitbe/user/service/UserService.java b/src/main/java/com/example/beginnerfitbe/user/service/UserService.java index 65c58e1..22c284c 100644 --- a/src/main/java/com/example/beginnerfitbe/user/service/UserService.java +++ b/src/main/java/com/example/beginnerfitbe/user/service/UserService.java @@ -2,6 +2,7 @@ import com.example.beginnerfitbe.error.StateResponse; +import com.example.beginnerfitbe.playlist.service.PlaylistService; import com.example.beginnerfitbe.s3.util.S3Uploader; import com.example.beginnerfitbe.user.domain.User; import com.example.beginnerfitbe.user.dto.UserDto; @@ -13,6 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -23,14 +25,18 @@ public class UserService { private final UserRepository userRepository; private final S3Uploader s3Uploader; + private final PlaylistService playlistService; public List list(){ return userRepository.findAll().stream() .map(UserDto::fromEntity) .collect(Collectors.toList()); } - public void create(User user) { + public void create(User user) throws IOException { userRepository.save(user); + + //플레이리스트 생성 + playlistService.create(user); } public UserDto read(Long id) { @@ -52,10 +58,19 @@ public ResponseEntity update(Long id, UserUpdateDto requestDto, M User user = userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("User not found")); String previousPictureUrl = user.getProfilePictureUrl(); + boolean createSignal = true; //정보 업데이트 - if (requestDto.getName() != null && requestDto.getExercisePurpose()!=0 && requestDto.getExercisePart()!=0 + if (requestDto.getName() != null && requestDto.getExercisePurpose()!=null && requestDto.getExercisePart()!=null && requestDto.getExerciseTime()!=0 && requestDto.getExerciseIntensity()!=0 ) { + //건강정보 안바뀜 -> 플레이리스트 그대로 + if(requestDto.getExercisePurpose().equals(user.getExercisePurpose()) + && requestDto.getExercisePart().equals(user.getExercisePart()) + && requestDto.getExerciseIntensity()==user.getExerciseIntensity() + && requestDto.getExerciseTime()==user.getExerciseTime()){ + createSignal=false; + } + user.update(requestDto.getName(), requestDto.getExercisePurpose(), requestDto.getExercisePart(), requestDto.getExerciseTime(), requestDto.getExerciseIntensity()); } @@ -72,6 +87,12 @@ public ResponseEntity update(Long id, UserUpdateDto requestDto, M } user.updatePicture(pictureUrl); userRepository.save(user); + + //플레이리스트 생성 + if(createSignal){ + playlistService.create(user); + } + return ResponseEntity.ok(StateResponse.builder().code("SUCCESS").message("정보를 성공적으로 업데이트했습니다.").build()); } catch (Exception e){ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(StateResponse.builder().code("ERROR").message("오류가 발생했습니다: " + e.getMessage()).build()); diff --git a/src/main/java/com/example/beginnerfitbe/youtube/controller/YoutubeVideoController.java b/src/main/java/com/example/beginnerfitbe/youtube/controller/YoutubeVideoController.java new file mode 100644 index 0000000..8177a1d --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/controller/YoutubeVideoController.java @@ -0,0 +1,70 @@ +package com.example.beginnerfitbe.youtube.controller; + +import com.example.beginnerfitbe.jwt.util.JwtUtil; +import com.example.beginnerfitbe.playlist.service.PlaylistService; +import com.example.beginnerfitbe.youtube.dto.YoutubeVideoDto; +import com.example.beginnerfitbe.youtube.service.YoutubeVideoService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/playlists") +@RequiredArgsConstructor +public class YoutubeVideoController { + + private final YoutubeVideoService youtubeVideoService; + private final PlaylistService playlistService; + private final JwtUtil jwtUtil; + + //비디오 선택 -> 시청 true -> 다 true면 플레이리스트 완료 true + @PostMapping("/videos/{videoId}") + @Operation(summary = "비디오 선택 메소드", description = "비디오를 선택하면 시청 한 상태로 업데이트 합니다.") + private ResponseEntity watchVideo(HttpServletRequest request, @PathVariable Long videoId) { + Long userId = jwtUtil.getUserId(jwtUtil.resolveToken(request).substring(7)); + + YoutubeVideoDto youtubeVideoDto = youtubeVideoService.watchVideo(userId, videoId); + + Long playlistId = youtubeVideoDto.getPlaylistId(); + playlistService.update(playlistId); + + return ResponseEntity.ok("비디오 시청 상태가 업데이트되었습니다."); + } + + @GetMapping("/videos") + @Operation(summary = "비디오 전체 조회 메소드", description = "전체 비디오를 조회합니다.") + private ResponseEntity list(){ + return ResponseEntity.ok(youtubeVideoService.list()); + } + + @GetMapping("/videos/{videoId}") + @Operation(summary = "비디오 상세 조회 메소드", description = "비디오의 상세 정보를 조회합니다.") + private ResponseEntity read(@PathVariable Long videoId){ + return ResponseEntity.ok(youtubeVideoService.read(videoId)); + } + + @GetMapping("/{playlistId}/videos") + @Operation(summary = "플레이리스트 별 비디오 조회", description = "플레이리스트에 해당하는 비디오를 조회합니다.") + private ResponseEntity getYoutubeVideosByPlaylist(@PathVariable Long playlistId){ + return ResponseEntity.ok(youtubeVideoService.getYoutubeVideosByPlaylist(playlistId)); + } + + @GetMapping("/videos/watched") + @Operation(summary = "시청한 비디오 목록 조회", description = "사용자가 시청했던 비디오 목록을 조회합니다.") + public ResponseEntity getWatchedVideo(HttpServletRequest request) { + Long userId = jwtUtil.getUserId(jwtUtil.resolveToken(request).substring(7)); + return ResponseEntity.ok(youtubeVideoService.getWatchedVideo(userId)); + } + + //홈화면 다음 비디오 재생 + @GetMapping("/videos/next") + @Operation(summary = "다음 비디오 조회", description = "사용자가 마지막으로 시청한 다음 비디오 정보를 조회합니다.") + public ResponseEntity getRecentVideo(HttpServletRequest request) { + Long userId = jwtUtil.getUserId(jwtUtil.resolveToken(request).substring(7)); + return ResponseEntity.ok(youtubeVideoService.getNextVideo(userId)); + } + +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/domain/YoutubeVideo.java b/src/main/java/com/example/beginnerfitbe/youtube/domain/YoutubeVideo.java new file mode 100644 index 0000000..f70d7bf --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/domain/YoutubeVideo.java @@ -0,0 +1,75 @@ +package com.example.beginnerfitbe.youtube.domain; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +public class YoutubeVideo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String videoId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String url; + + @Column(nullable = false) + private String thumbnail; + + @Column + private String description; + + @Column(nullable = false) + private String channel; + + @Column(nullable = false) + private String publishedAt; + + @Column(nullable = false) + private String duration; + + @Column(nullable = false) + private Boolean isWatched; + + @Column + private LocalDateTime watchedTime; + + @ManyToOne + @JoinColumn(name = "playlist_id", nullable = false) + private Playlist playlist; + + @Builder + public YoutubeVideo(String title, String videoId, String url, String thumbnail, String description, String channel, String publishedAt, String duration, Boolean isWatched, LocalDateTime watchedTime, Playlist playlist) { + this.title = title; + this.videoId = videoId; + this.url = url; + this.thumbnail = thumbnail; + this.description = description; + this.channel = channel; + this.publishedAt = publishedAt; + this.duration = duration; + this.isWatched = isWatched; + this.watchedTime = watchedTime; + this.playlist = playlist; + } + + public void updatePlaylist(Playlist playlist) { + this.playlist = playlist; + } + public void watched(Boolean isWatched){ + this.isWatched = isWatched; + this.watchedTime = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/dto/SearchResDto.java b/src/main/java/com/example/beginnerfitbe/youtube/dto/SearchResDto.java new file mode 100644 index 0000000..cd81127 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/dto/SearchResDto.java @@ -0,0 +1,52 @@ +package com.example.beginnerfitbe.youtube.dto; + +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class SearchResDto { + private String videoId; + private String title; + private String url; + private String description; + private String thumbnail; + private String channel; + private String publishedAt; + private String duration; + private Boolean isWatched; + private LocalDateTime watchedTime; + + @Builder + public SearchResDto(String videoId, String title, String url, String description, String thumbnail, String channel, String publishedAt, String duration, Boolean isWatched ,LocalDateTime watchedTime) { + this.videoId = videoId; + this.title = title; + this.url = url; + this.description = description; + this.thumbnail = thumbnail; + this.channel = channel; + this.publishedAt = publishedAt; + this.duration = duration; + this. isWatched = isWatched; + this. watchedTime =watchedTime; + } + + public YoutubeVideo toEntity(){ + return YoutubeVideo.builder() + .videoId(videoId) + .title(title) + .url(url) + .description(description) + .thumbnail(thumbnail) + .channel(channel) + .publishedAt(publishedAt) + .duration(duration) + .isWatched(false) + .watchedTime(null) + .build(); + } +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/dto/SelectedVideoDto.java b/src/main/java/com/example/beginnerfitbe/youtube/dto/SelectedVideoDto.java new file mode 100644 index 0000000..929ae72 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/dto/SelectedVideoDto.java @@ -0,0 +1,14 @@ +package com.example.beginnerfitbe.youtube.dto; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import lombok.Data; + +import java.util.List; + +@Data +public class SelectedVideoDto { + private List youtubeVideos; + private String totalTime; + private Playlist playlist; +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeVideoDto.java b/src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeVideoDto.java new file mode 100644 index 0000000..e1c2ea0 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeVideoDto.java @@ -0,0 +1,55 @@ +package com.example.beginnerfitbe.youtube.dto; + +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class YoutubeVideoDto { + private Long id; + private String title; + private String url; + private String thumbnail; + private String description; + private String channel; + private String publishedAt; + private String duration; + private Boolean isWatched; + private LocalDateTime watchedTime; + private Long playlistId; + + @Builder + public YoutubeVideoDto(Long id, String videoId, String title, String url, String thumbnail, String description, String channel, String publishedAt, String duration, Boolean isWatched, LocalDateTime watchedTime, Long playlistId) { + this.id = id; + this.title = title; + this.url = url; + this.thumbnail = thumbnail; + this.description = description; + this.channel = channel; + this.publishedAt = publishedAt; + this.duration = duration; + this.isWatched = isWatched; + this.watchedTime = watchedTime; + this.playlistId = playlistId; + } + + public static YoutubeVideoDto fromEntity(YoutubeVideo youtubeVideo) { + return YoutubeVideoDto.builder() + .id(youtubeVideo.getId()) + .title(youtubeVideo.getTitle()) + .url(youtubeVideo.getUrl()) + .thumbnail(youtubeVideo.getThumbnail()) + .description(youtubeVideo.getDescription()) + .channel(youtubeVideo.getChannel()) + .publishedAt(youtubeVideo.getPublishedAt()) + .duration(youtubeVideo.getDuration()) + .isWatched(youtubeVideo.getIsWatched()) + .watchedTime(youtubeVideo.getWatchedTime()) + .playlistId(youtubeVideo.getPlaylist().getId()) + .build(); + } +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java b/src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java new file mode 100644 index 0000000..41c6c5c --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java @@ -0,0 +1,15 @@ +package com.example.beginnerfitbe.youtube.repository; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface YoutubeVideoRepository extends JpaRepository { + List findVideosByPlaylist(Playlist playlist); + List findByPlaylist_UserId(Long userId); + YoutubeVideo findFirstByPlaylist_UserIdOrderByWatchedTimeDesc(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java b/src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java new file mode 100644 index 0000000..8760e33 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java @@ -0,0 +1,110 @@ +package com.example.beginnerfitbe.youtube.service; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import com.example.beginnerfitbe.playlist.repository.PlaylistRepository; +import com.example.beginnerfitbe.user.domain.User; +import com.example.beginnerfitbe.user.repository.UserRepository; +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import com.example.beginnerfitbe.youtube.dto.SelectedVideoDto; +import com.example.beginnerfitbe.youtube.dto.YoutubeVideoDto; +import com.example.beginnerfitbe.youtube.repository.YoutubeVideoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +@Service +@RequiredArgsConstructor +public class YoutubeVideoService { + + private final YoutubeVideoRepository youtubeVideoRepository; + private final PlaylistRepository playlistRepository; + private final UserRepository userRepository; + + + public void create(SelectedVideoDto playlistDto, Playlist playlist){ + List youtubeVideos = playlistDto.getYoutubeVideos(); + youtubeVideos.forEach(video -> video.updatePlaylist(playlist)); + youtubeVideoRepository.saveAll(youtubeVideos); + } + + @Transactional + public YoutubeVideoDto watchVideo(Long userId ,Long videoId) { + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found")); + YoutubeVideo youtubeVideo = youtubeVideoRepository.findById(videoId).orElseThrow(() -> new IllegalArgumentException("Video not found")); + if(!user.getId().equals( youtubeVideo.getPlaylist().getUser().getId())) throw new IllegalArgumentException("재생목록 주인만 시청 상태 변경 가능."); + + //시청 true + youtubeVideo.watched(true); + youtubeVideoRepository.save(youtubeVideo); + + return YoutubeVideoDto.fromEntity(youtubeVideo); + } + + //비디오 조회 + public List list(){ + return youtubeVideoRepository.findAll().stream() + .map(YoutubeVideoDto::fromEntity) + .collect(Collectors.toList()); + } + + public YoutubeVideoDto read(Long videoId){ + YoutubeVideo youtubeVideo = youtubeVideoRepository.findById(videoId).orElseThrow(() -> new IllegalArgumentException("Video not found")); + + return YoutubeVideoDto.fromEntity(youtubeVideo); + } + public List getYoutubeVideosByPlaylist(Long playlistId) { + Playlist playlist = playlistRepository.findById(playlistId).orElseThrow(() -> new IllegalArgumentException("Playlist not found")); + return youtubeVideoRepository.findVideosByPlaylist(playlist).stream() + .map(YoutubeVideoDto::fromEntity) + .collect(Collectors.toList()); + } + + // 사용자 비디오 목록 조회 + public List getWatchedVideo(Long userId) { + userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found")); + return youtubeVideoRepository.findByPlaylist_UserId(userId).stream() + .filter(YoutubeVideo::getIsWatched) // 시청한 비디오만 필터링 + .map(YoutubeVideoDto::fromEntity) + .collect(Collectors.toList()); + } + + // 다음 영상 + public YoutubeVideoDto getNextVideo(Long userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found: ")); + + // 가장 최근 플레이리스트 조회 + Playlist recentPlaylist = playlistRepository.findFirstByUserOrderByCreatedAtDesc(user) + .orElseThrow(() -> new IllegalArgumentException("Recent playlist not found")); + + List videos = youtubeVideoRepository.findVideosByPlaylist(recentPlaylist); + if (videos.isEmpty()) { + throw new IllegalArgumentException("video not found"); + } + + // 최근 시청 비디오 조회 + YoutubeVideo lastWatchedVideo = youtubeVideoRepository.findFirstByPlaylist_UserIdOrderByWatchedTimeDesc(userId); + + // 최근 시청 비디오가 없는 경우 첫 번째 비디오 반환 + if (!lastWatchedVideo.getIsWatched()) { + System.out.println("아직 아무것도 안봄"); + return YoutubeVideoDto.fromEntity(videos.get(0)); + } + + int lastWatchedIndex = videos.indexOf(lastWatchedVideo); + + if (lastWatchedIndex == videos.size()-1) { + System.out.println(" 다 봄"); + return null; + } + + YoutubeVideo nextVideo = videos.get(lastWatchedIndex+1); + return YoutubeVideoDto.fromEntity(nextVideo); + } + + +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java b/src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java new file mode 100644 index 0000000..a289fb8 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java @@ -0,0 +1,212 @@ +package com.example.beginnerfitbe.youtube.util; + +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import com.example.beginnerfitbe.youtube.dto.SearchResDto; +import com.example.beginnerfitbe.youtube.dto.SelectedVideoDto; +import com.example.beginnerfitbe.youtube.repository.YoutubeVideoRepository; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class YoutubeUtil { + + @Value("${youtube.api.key}") + private String apiKey; + + // 비디오 검색 + public List search(String keyword) throws IOException { + JsonFactory jsonFactory = new JacksonFactory(); + YouTube youtube = new YouTube.Builder( + new com.google.api.client.http.javanet.NetHttpTransport(), + jsonFactory, + request -> { + }) + .setApplicationName("youtube-search") + .build(); + + YouTube.Search.List search = youtube.search().list(Collections.singletonList("id,snippet")); + + search.setKey(apiKey); + search.setQ(keyword); + search.setMaxResults(20L); + + SearchListResponse searchResponse = search.execute(); + List searchResultList = searchResponse.getItems(); + + if (searchResultList != null && !searchResultList.isEmpty()) { + return searchResultList.stream() + .filter(searchResult -> "youtube#video".equals(searchResult.getId().getKind())) + .map(searchResult -> { + String videoId = searchResult.getId().getVideoId(); + String videoTitle = searchResult.getSnippet().getTitle(); + String videoDescription = searchResult.getSnippet().getDescription(); + String videoChannel = searchResult.getSnippet().getChannelTitle(); + String publishedAt = searchResult.getSnippet().getPublishedAt().toString(); + String videoThumbnail = searchResult.getSnippet().getThumbnails().getHigh().getUrl(); + String duration = videoInfo(youtube, videoId); + + return SearchResDto.builder() + .videoId(videoId) + .url("https://www.youtube.com/watch?v=" + videoId) + .title(videoTitle) + .description(videoDescription) + .channel(videoChannel) + .publishedAt(publishedAt) + .thumbnail(videoThumbnail) + .duration(duration) + .build(); + }) + .collect(Collectors.toList()); + } + System.out.println("No search results found."); + return Collections.emptyList(); + } + + // 비디오 메타데이터 + public String videoInfo(YouTube youtube, String videoId) { + try { + YouTube.Videos.List videosListByIdRequest = youtube.videos().list(Collections.singletonList("contentDetails")); + videosListByIdRequest.setKey(apiKey); + + videosListByIdRequest.setId(Collections.singletonList(videoId)); + VideoListResponse videoListResponse = videosListByIdRequest.execute(); + + List