Skip to content

Commit

Permalink
Feat/#515 캐러셀용 최신 노래 조회 API 구현 (#517)
Browse files Browse the repository at this point in the history
* feat: 캐러셀용 최신 노래 조회 API 구현

* fix: voting_song_part 테이블 삭제 스키마 추가

* feat: 캐러셀용 최신 노래 조회 API 구현

* fix: voting_song_part 테이블 삭제 스키마 추가

* fix: 킬링파트 길이 수정

* feat: 캐러셀용 최신 노래 조회 API 구현

* fix: voting_song_part 테이블 삭제 스키마 추가

* feat: 캐러셀용 최신 노래 조회 API 구현

* fix: 킬링파트 길이 수정

* fix: Artist 엔티티 병합, 실패하는 테스트 수정

---------

Co-authored-by: 스플릿 <[email protected]>
  • Loading branch information
somsom13 and splitCoding authored Oct 19, 2023
1 parent fced3bd commit fa8be3c
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -13,8 +14,10 @@
import shook.shook.member.domain.Member;
import shook.shook.member.domain.repository.MemberRepository;
import shook.shook.member.exception.MemberException;
import shook.shook.song.application.dto.RecentSongCarouselResponse;
import shook.shook.member_part.domain.MemberPart;
import shook.shook.member_part.domain.repository.MemberPartRepository;
import shook.shook.song.application.dto.RecentSongCarouselResponse;
import shook.shook.song.application.dto.SongResponse;
import shook.shook.song.application.dto.SongSwipeResponse;
import shook.shook.song.application.dto.SongWithKillingPartsRegisterRequest;
Expand Down Expand Up @@ -232,4 +235,12 @@ public SongResponse findSongById(final Long songId, final MemberInfo memberInfo)

return SongResponse.of(song, likedKillingPartIds, memberPart);
}

public List<RecentSongCarouselResponse> findRecentRegisteredSongsForCarousel(final Integer size) {
final List<Song> topSongs = songRepository.findSongsOrderById(PageRequest.of(0, size));

return topSongs.stream()
.map(RecentSongCarouselResponse::from)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package shook.shook.song.application.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import shook.shook.song.domain.Song;

@Schema(description = "캐러셀에 보여질 최근 노래 응답")
@AllArgsConstructor
@Getter
public class RecentSongCarouselResponse {

@Schema(description = "노래 id", example = "1")
private final Long id;

@Schema(description = "노래 제목", example = "노래제목")
private final String title;

@Schema(description = "가수 이름", example = "가수")
private final String singer;

@Schema(description = "비디오 영상 길이", example = "274")
private final int videoLength;

@Schema(description = "앨범 자켓 이미지 url", example = "https://image.com/album_cover.jpg")
private final String albumCoverUrl;

public static RecentSongCarouselResponse from(final Song song) {
return new RecentSongCarouselResponse(
song.getId(),
song.getTitle(),
song.getArtistName(),
song.getLength(),
song.getAlbumCoverUrl()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ List<Song> findSongsWithMoreLikeCountThanSongWithId(
final Pageable pageable
);

@Query("SELECT s from Song s ORDER BY s.id DESC")
List<Song> findSongsOrderById(final Pageable pageable);

boolean existsSongByTitle(final SongTitle title);

@Query("SELECT s AS song, SUM(COALESCE(kp.likeCount, 0)) AS totalLikeCount "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package shook.shook.song.ui;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import shook.shook.song.application.SongService;
import shook.shook.song.application.dto.RecentSongCarouselResponse;
import shook.shook.song.ui.openapi.CarouselSongApi;

@RequiredArgsConstructor
@RequestMapping("/songs/recent")
@RestController
public class CarouselSongController implements CarouselSongApi {

private final SongService songService;

@GetMapping
public ResponseEntity<List<RecentSongCarouselResponse>> findRecentSongsForCarousel(
@RequestParam(name = "size", defaultValue = "5", required = false) final Integer size
) {
final List<RecentSongCarouselResponse> responses = songService.findRecentRegisteredSongsForCarousel(size);

return ResponseEntity.ok(responses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package shook.shook.song.ui.openapi;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import shook.shook.song.application.dto.RecentSongCarouselResponse;

@Tag(name = "Carousel Songs", description = "메인페이지 캐러셀 조회 API")
public interface CarouselSongApi {

@Operation(
summary = "캐러셀에 들어갈 노래 반환",
description = "캐러셀에 들어갈 노래 5개를 등록 최신 순 리스트로 반환한다."
)
@ApiResponse(
responseCode = "200",
description = "최근에 등록된 노래 리스트 조회 성공"
)
@Parameter(
name = "size",
description = "조회할 개수",
example = "4"
)
@GetMapping
ResponseEntity<List<RecentSongCarouselResponse>> findRecentSongsForCarousel(
@RequestParam(name = "size", defaultValue = "5", required = false) final Integer size
);
}
1 change: 1 addition & 0 deletions backend/src/main/resources/dev/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ drop table if exists killing_part_like;
drop table if exists killing_part_comment;
drop table if exists voting_song_part;
drop table if exists voting_song;
drop table if exists voting_song_part;
drop table if exists vote;
drop table if exists member;
drop table if exists artist;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import shook.shook.member_part.domain.MemberPart;
import shook.shook.member_part.domain.repository.MemberPartRepository;
import shook.shook.song.application.dto.KillingPartRegisterRequest;
import shook.shook.song.application.dto.RecentSongCarouselResponse;
import shook.shook.song.application.dto.MemberPartResponse;
import shook.shook.song.application.dto.SongResponse;
import shook.shook.song.application.dto.SongSwipeResponse;
Expand Down Expand Up @@ -538,4 +539,28 @@ void findSongById() {
() -> assertThat(response.getMemberPart().getId()).isNotNull()
);
}

@DisplayName("최근에 등록된 순으로 노래 5개를 조회한다.")
@Test
void findRecentRegisteredSongsForCarousel() {
// given
registerNewSong("노래1");
registerNewSong("노래2");
registerNewSong("노래3");
registerNewSong("노래4");
registerNewSong("노래5");
registerNewSong("노래6");
registerNewSong("노래7");

saveAndClearEntityManager();

// when
final List<RecentSongCarouselResponse> songs = songService.findRecentRegisteredSongsForCarousel(5);

// then
assertThat(songs.stream()
.map(RecentSongCarouselResponse::getId)
.toList())
.containsExactly(7L, 6L, 5L, 4L, 3L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private Song createNewSongWithKillingParts() {
final KillingPart thirdKillingPart = KillingPart.forSave(20, 5);

final Artist artist = new Artist("image", "name");
artistRepository.save(artist);
return new Song(
"title",
"3rUPND6FG8A",
Expand Down Expand Up @@ -438,4 +439,21 @@ void findAllSongsWithTotalLikeCountByArtist() {
() -> assertThat(result.get(0).getTotalLikeCount()).isEqualTo(4)
);
}

@DisplayName("노래 최신순으로 정렬하여 상위 노래를 조회한다.")
@Test
void findSongsOrderById() {
// given
final Song song1 = songRepository.save(createNewSongWithKillingParts());
final Song song2 = songRepository.save(createNewSongWithKillingParts());
final Song song3 = songRepository.save(createNewSongWithKillingParts());
final Song song4 = songRepository.save(createNewSongWithKillingParts());
final Song song5 = songRepository.save(createNewSongWithKillingParts());

// when
final List<Song> songs = songRepository.findSongsOrderById(PageRequest.of(0, 4));

// then
assertThat(songs).containsExactly(song5, song4, song3, song2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package shook.shook.song.ui;

import static org.assertj.core.api.Assertions.assertThat;

import io.restassured.RestAssured;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.jdbc.Sql;
import shook.shook.song.application.dto.RecentSongCarouselResponse;
import shook.shook.song.domain.Artist;
import shook.shook.song.domain.Genre;
import shook.shook.song.domain.KillingParts;
import shook.shook.song.domain.Song;
import shook.shook.song.domain.killingpart.KillingPart;
import shook.shook.song.domain.repository.ArtistRepository;
import shook.shook.song.domain.repository.SongRepository;

@Sql("classpath:/killingpart/initialize_killing_part_song.sql")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class CarouselSongControllerTest {

@LocalServerPort
private int port;

@Autowired
private SongRepository songRepository;

@Autowired
private ArtistRepository artistRepository;

@BeforeEach
void setUp() {
RestAssured.port = port;
}

@DisplayName("캐러셀에 보여질 노래들을 조회하면 200 상태코드와 id 높은 순 노래 데이터가 반환된다.")
@Test
void findRecentSongsForCarousel() {
// given
songRepository.findById(3L).get();
songRepository.findById(4L).get();
songRepository.save(createNewSongWithKillingParts());
songRepository.save(createNewSongWithKillingParts());

// when
final List<RecentSongCarouselResponse> response = RestAssured.given().log().all()
.param("size", 4)
.when().log().all()
.get("/songs/recent")
.then().log().all()
.statusCode(HttpStatus.OK.value())
.extract()
.body().jsonPath().getList(".", RecentSongCarouselResponse.class);

// then
assertThat(response).hasSize(4);
assertThat(response.stream()
.map(RecentSongCarouselResponse::getId)
.toList())
.containsExactly(6L, 5L, 4L, 3L);
}

@DisplayName("캐러셀에 보여질 노래들을 조회할 때, size 파라미터가 전달되지 않으면 기본값인 5개가 조회된다.")
@Test
void findRecentSongsForCarousel_noParam() {
// given
songRepository.findById(3L).get();
songRepository.findById(4L).get();
songRepository.save(createNewSongWithKillingParts());
songRepository.save(createNewSongWithKillingParts());
songRepository.save(createNewSongWithKillingParts());

// when
final List<RecentSongCarouselResponse> response = RestAssured.given().log().all()
.when().log().all()
.get("/songs/recent")
.then().log().all()
.statusCode(HttpStatus.OK.value())
.extract()
.body().jsonPath().getList(".", RecentSongCarouselResponse.class);

// then
assertThat(response).hasSize(5);
assertThat(response.stream()
.map(RecentSongCarouselResponse::getId)
.toList())
.containsExactly(7L, 6L, 5L, 4L, 3L);
}

private Song createNewSongWithKillingParts() {
final KillingPart firstKillingPart = KillingPart.forSave(10, 5);
final KillingPart secondKillingPart = KillingPart.forSave(15, 5);
final KillingPart thirdKillingPart = KillingPart.forSave(20, 5);

final Artist artist = new Artist("image", "name");
artistRepository.save(artist);
return new Song(
"제목", "비디오ID는 11글자", "이미지URL", artist, 5, Genre.from("댄스"),
new KillingParts(List.of(firstKillingPart, secondKillingPart, thirdKillingPart)));
}
}

0 comments on commit fa8be3c

Please sign in to comment.