diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 832db7b6e..5afc0e13a 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -54,6 +54,12 @@ public enum ErrorCode { CAN_NOT_READ_SONG_DATA_FILE(3011, "노래 데이터 파일을 읽을 수 없습니다."), SONG_ALREADY_EXIST(3012, "등록하려는 노래가 이미 존재합니다."), WRONG_GENRE_TYPE(3013, "잘못된 장르 타입입니다."), + EMPTY_ARTIST_PROFILE_URL(3014, "가수 프로필 이미지는 비어있을 수 없습니다."), + TOO_LONG_ARTIST_PROFILE_URL(3015, "가수 프로필 이미지URL은 65,356자를 넘길 수 없습니다."), + EMPTY_ARTIST_SYNONYM(3016, "가수 동의어는 비어있을 수 없습니다."), + TOO_LONG_ARTIST_SYNONYM(3017, "가수 동의어는 255자를 넘길 수 없습니다."), + ARTIST_NOT_EXIST(3018, "존재하지 않는 가수입니다."), + // 4000: 투표 diff --git a/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java b/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java index 5a7bb1162..0063ad425 100644 --- a/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java +++ b/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java @@ -15,6 +15,7 @@ import shook.shook.member.exception.MemberException; import shook.shook.member_part.exception.MemberPartException; import shook.shook.part.exception.PartException; +import shook.shook.song.exception.ArtistException; import shook.shook.song.exception.SongException; import shook.shook.song.exception.killingpart.KillingPartCommentException; import shook.shook.song.exception.killingpart.KillingPartException; @@ -57,6 +58,7 @@ public ResponseEntity handleTokenException(final CustomException VotingSongException.class, VotingSongPartException.PartNotExistException.class, PartException.class, + ArtistException.class, MemberPartException.class }) public ResponseEntity handleGlobalBadRequestException(final CustomException e) { diff --git a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java new file mode 100644 index 000000000..ba72c315e --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java @@ -0,0 +1,120 @@ +package shook.shook.song.application; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shook.shook.song.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.InMemoryArtistSynonyms; +import shook.shook.song.domain.InMemoryArtistSynonymsGenerator; +import shook.shook.song.domain.Song; +import shook.shook.song.domain.repository.ArtistRepository; +import shook.shook.song.domain.repository.SongRepository; +import shook.shook.song.domain.repository.dto.SongTotalLikeCountDto; +import shook.shook.song.exception.ArtistException; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ArtistSearchService { + + private static final int TOP_SONG_COUNT_OF_ARTIST = 3; + + private final InMemoryArtistSynonyms inMemoryArtistSynonyms; + private final ArtistRepository artistRepository; + private final SongRepository songRepository; + private final InMemoryArtistSynonymsGenerator generator; + + public List searchArtistsByKeyword(final String keyword) { + final List artists = findArtistsStartsWithKeyword(keyword); + + return artists.stream() + .map(ArtistResponse::from) + .toList(); + } + + private List findArtistsStartsWithKeyword(final String keyword) { + final List artistsFoundByName = inMemoryArtistSynonyms.findAllArtistsNameStartsWith( + keyword); + final List artistsFoundBySynonym = inMemoryArtistSynonyms.findAllArtistsHavingSynonymStartsWith( + keyword); + + return removeDuplicateArtistResultAndSortByName(artistsFoundByName, artistsFoundBySynonym); + } + + private List removeDuplicateArtistResultAndSortByName(final List firstResult, + final List secondResult) { + return Stream.concat(firstResult.stream(), secondResult.stream()) + .distinct() + .sorted(Comparator.comparing(Artist::getArtistName)) + .toList(); + } + + public List searchArtistsAndTopSongsByKeyword( + final String keyword) { + final List artists = findArtistsStartsOrEndsWithKeyword(keyword); + + return artists.stream() + .map(artist -> ArtistWithSongSearchResponse.of( + artist, + getSongsOfArtistSortedByLikeCount(artist).size(), + getTopSongsOfArtist(artist)) + ) + .toList(); + } + + private List findArtistsStartsOrEndsWithKeyword(final String keyword) { + final List artistsFoundByName = inMemoryArtistSynonyms.findAllArtistsNameStartsOrEndsWith( + keyword); + final List artistsFoundBySynonym = inMemoryArtistSynonyms.findAllArtistsHavingSynonymStartsOrEndsWith( + keyword); + + return removeDuplicateArtistResultAndSortByName(artistsFoundByName, artistsFoundBySynonym); + } + + private List getTopSongsOfArtist(final Artist artist) { + final List songs = getSongsOfArtistSortedByLikeCount(artist); + if (songs.size() < TOP_SONG_COUNT_OF_ARTIST) { + return songs; + } + + return songs.subList(0, TOP_SONG_COUNT_OF_ARTIST); + } + + private List getSongsOfArtistSortedByLikeCount(final Artist artist) { + final List songsWithTotalLikeCount = songRepository.findAllSongsWithTotalLikeCountByArtist( + artist); + + return songsWithTotalLikeCount.stream() + .sorted(Comparator.comparing(SongTotalLikeCountDto::getTotalLikeCount, + Comparator.reverseOrder()) + .thenComparing(songWithTotalLikeCount -> songWithTotalLikeCount.getSong().getId(), + Comparator.reverseOrder()) + ) + .map(SongTotalLikeCountDto::getSong) + .toList(); + } + + public ArtistWithSongSearchResponse searchAllSongsByArtist(final long artistId) { + final Artist artist = findArtistById(artistId); + final List songs = getSongsOfArtistSortedByLikeCount(artist); + + return ArtistWithSongSearchResponse.of(artist, songs.size(), songs); + } + + private Artist findArtistById(final long artistId) { + return artistRepository.findById(artistId) + .orElseThrow(() -> new ArtistException.NotExistException( + Map.of("ArtistId", String.valueOf(artistId)) + )); + } + + public void updateArtistSynonymFromDatabase() { + generator.initialize(); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java index 67dec6805..4e71ed352 100644 --- a/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java +++ b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java @@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; import shook.shook.song.domain.Song; @@ -90,7 +91,10 @@ private Optional parseToSong(final Row currentRow) { final Optional killingParts = getKillingParts(cellIterator); return killingParts.map( - parts -> new Song(title, videoId, albumCoverUrl, singer, length, Genre.from(genre), parts)); + parts -> new Song(title, videoId, albumCoverUrl, + new Artist("image", "name"), length, + Genre.from(genre), + parts)); } private String getString(final Iterator iterator) { diff --git a/backend/src/main/java/shook/shook/song/application/SongService.java b/backend/src/main/java/shook/shook/song/application/SongService.java index b4827e3af..7484e252a 100644 --- a/backend/src/main/java/shook/shook/song/application/SongService.java +++ b/backend/src/main/java/shook/shook/song/application/SongService.java @@ -22,11 +22,10 @@ import shook.shook.song.domain.Genre; import shook.shook.song.domain.InMemorySongs; import shook.shook.song.domain.Song; -import shook.shook.song.domain.SongTitle; import shook.shook.song.domain.killingpart.repository.KillingPartLikeRepository; import shook.shook.song.domain.killingpart.repository.KillingPartRepository; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.song.domain.repository.SongRepository; -import shook.shook.song.exception.SongException; @RequiredArgsConstructor @Transactional(readOnly = true) @@ -43,6 +42,7 @@ public class SongService { private final MemberRepository memberRepository; private final MemberPartRepository memberPartRepository; private final InMemorySongs inMemorySongs; + private final ArtistRepository artistRepository; private final SongDataExcelReader songDataExcelReader; @Transactional @@ -53,9 +53,7 @@ public Long register(final SongWithKillingPartsRegisterRequest request) { } private Song saveSong(final Song song) { - if (songRepository.existsSongByTitle(new SongTitle(song.getTitle()))) { - throw new SongException.SongAlreadyExistException(Map.of("Song-Name", song.getTitle())); - } + artistRepository.save(song.getArtist()); final Song savedSong = songRepository.save(song); killingPartRepository.saveAll(song.getKillingParts()); return savedSong; diff --git a/backend/src/main/java/shook/shook/song/application/dto/ArtistResponse.java b/backend/src/main/java/shook/shook/song/application/dto/ArtistResponse.java new file mode 100644 index 000000000..b710f705c --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/ArtistResponse.java @@ -0,0 +1,30 @@ +package shook.shook.song.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import shook.shook.song.domain.Artist; + +@Schema(description = "아티스트를 통한 아티스트, 해당 아티스트의 노래 검색 결과") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class ArtistResponse { + + @Schema(description = "아티스트 id", example = "1") + private final Long id; + + @Schema(description = "가수 이름", example = "가수") + private final String singer; + + @Schema(description = "가수 대표 이미지 url", example = "https://image.com/artist-profile.jpg") + private final String profileImageUrl; + + public static ArtistResponse from(final Artist artist) { + return new ArtistResponse( + artist.getId(), + artist.getArtistName(), + artist.getProfileImageUrl() + ); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java b/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java new file mode 100644 index 000000000..eaabf48c6 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java @@ -0,0 +1,48 @@ +package shook.shook.song.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.Song; + +@Schema(description = "아티스트를 통한 아티스트, 해당 아티스트의 노래 검색 결과") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class ArtistWithSongSearchResponse { + + @Schema(description = "아티스트 id", example = "1") + private final Long id; + + @Schema(description = "가수 이름", example = "가수") + private final String singer; + + @Schema(description = "가수 대표 이미지 url", example = "https://image.com/artist-profile.jpg") + private final String profileImageUrl; + + @Schema(description = "가수 노래 총 개수", example = "10") + private final int totalSongCount; + + @Schema(description = "아티스트의 노래 목록") + private final List songs; + + public static ArtistWithSongSearchResponse of(final Artist artist, final int totalSongCount, + final List songs) { + return new ArtistWithSongSearchResponse( + artist.getId(), + artist.getArtistName(), + artist.getProfileImageUrl(), + totalSongCount, + convertToSongSearchResponse(songs, artist.getArtistName()) + ); + } + + private static List convertToSongSearchResponse(final List songs, + final String singer) { + return songs.stream() + .map(song -> SongSearchResponse.from(song, singer)) + .toList(); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/LikedKillingPartResponse.java b/backend/src/main/java/shook/shook/song/application/dto/LikedKillingPartResponse.java index d361bba07..2f6e81c66 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/LikedKillingPartResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/LikedKillingPartResponse.java @@ -37,7 +37,7 @@ public static LikedKillingPartResponse of(final Song song, final KillingPart kil return new LikedKillingPartResponse( song.getId(), song.getTitle(), - song.getSinger(), + song.getArtistName(), song.getAlbumCoverUrl(), killingPart.getId(), killingPart.getStartSecond(), diff --git a/backend/src/main/java/shook/shook/song/application/dto/MyPartsResponse.java b/backend/src/main/java/shook/shook/song/application/dto/MyPartsResponse.java index 4b5e0c079..23bd085a3 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/MyPartsResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/MyPartsResponse.java @@ -41,7 +41,7 @@ public static MyPartsResponse of(final Song song, final MemberPart memberPart) { song.getId(), song.getTitle(), song.getVideoId(), - song.getSinger(), + song.getArtistName(), song.getAlbumCoverUrl(), memberPart.getId(), memberPart.getStartSecond(), diff --git a/backend/src/main/java/shook/shook/song/application/dto/SongResponse.java b/backend/src/main/java/shook/shook/song/application/dto/SongResponse.java index 434e8e648..19bfcd3e1 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/SongResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/SongResponse.java @@ -48,7 +48,7 @@ public static SongResponse of(final Song song, final List likedKillingPart return new SongResponse( song.getId(), song.getTitle(), - song.getSinger(), + song.getArtistName(), song.getLength(), song.getVideoId(), song.getAlbumCoverUrl(), diff --git a/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java b/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java new file mode 100644 index 000000000..bc32b2d4a --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java @@ -0,0 +1,38 @@ +package shook.shook.song.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import shook.shook.song.domain.Song; + +@Schema(description = "검색 결과 (가수, 가수의 노래) 응답") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class SongSearchResponse { + + @Schema(description = "노래 id", example = "1") + private final Long id; + + @Schema(description = "노래 제목", example = "제목") + private final String title; + + @Schema(description = "노래 앨범 커버 이미지 url", example = "https://image.com/album-cover.jpg") + private final String albumCoverUrl; + + @Schema(description = "노래 비디오 길이", example = "247") + private final int videoLength; + + @Schema(description = "가수 이름", example = "가수") + private final String singer; + + public static SongSearchResponse from(final Song song, final String singer) { + return new SongSearchResponse( + song.getId(), + song.getTitle(), + song.getAlbumCoverUrl(), + song.getLength(), + singer + ); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/dto/SongWithKillingPartsRegisterRequest.java b/backend/src/main/java/shook/shook/song/application/dto/SongWithKillingPartsRegisterRequest.java index 58494018e..cdb9feb7a 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/SongWithKillingPartsRegisterRequest.java +++ b/backend/src/main/java/shook/shook/song/application/dto/SongWithKillingPartsRegisterRequest.java @@ -10,6 +10,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; import shook.shook.song.domain.Song; @@ -34,7 +35,11 @@ public class SongWithKillingPartsRegisterRequest { @Schema(description = "가수 이름", example = "가수") @NotBlank - private String singer; + private String artistName; + + @Schema(description = "가수 프로필 이미지", example = "https://image.com/singer-profile.jpg") + @NotBlank + private String profileImageUrl; @Schema(description = "노래 길이", example = "247") @NotNull @@ -50,8 +55,15 @@ public class SongWithKillingPartsRegisterRequest { private List killingParts; public Song convertToSong() { - return new Song(title, videoId, imageUrl, singer, length, Genre.from(genre), - convertToKillingParts()); + return new Song( + title, + videoId, + imageUrl, + new Artist(profileImageUrl, artistName), + length, + Genre.from(genre), + convertToKillingParts() + ); } private KillingParts convertToKillingParts() { diff --git a/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java b/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java index 41aed8dc0..30e7aa7ae 100644 --- a/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java +++ b/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java @@ -34,7 +34,7 @@ private static HighLikedSongResponse from(final Song song) { return new HighLikedSongResponse( song.getId(), song.getTitle(), - song.getSinger(), + song.getArtistName(), song.getAlbumCoverUrl(), song.getTotalLikeCount(), song.getGenre().name() diff --git a/backend/src/main/java/shook/shook/song/domain/Artist.java b/backend/src/main/java/shook/shook/song/domain/Artist.java new file mode 100644 index 000000000..67eada87a --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -0,0 +1,82 @@ +package shook.shook.song.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "artist") +@Entity +public class Artist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private ProfileImageUrl profileImageUrl; + + @Embedded + private ArtistName artistName; + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); + + @PrePersist + private void prePersist() { + createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); + } + + public Artist(final String profileImageUrl, final String artistName) { + this.profileImageUrl = new ProfileImageUrl(profileImageUrl); + this.artistName = new ArtistName(artistName); + } + + public boolean nameStartsWith(final String keyword) { + return artistName.startsWithIgnoringCaseAndWhiteSpace(keyword); + } + + public boolean nameEndsWith(final String keyword) { + return artistName.endsWithIgnoringCaseAndWhiteSpace(keyword); + } + + public String getArtistName() { + return artistName.getValue(); + } + + public String getProfileImageUrl() { + return profileImageUrl.getValue(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Artist artist = (Artist) o; + if (Objects.isNull(artist.id) || Objects.isNull(this.id)) { + return false; + } + return Objects.equals(id, artist.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/ArtistName.java b/backend/src/main/java/shook/shook/song/domain/ArtistName.java new file mode 100644 index 000000000..a3198e596 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.java @@ -0,0 +1,68 @@ +package shook.shook.song.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.util.Map; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import shook.shook.song.exception.ArtistException; +import shook.shook.util.StringChecker; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode +@Embeddable +public class ArtistName { + + private static final int NAME_MAXIMUM_LENGTH = 50; + private static final String BLANK = "\\s"; + + @Column(name = "name", length = 50, nullable = false) + private String value; + + public ArtistName(final String value) { + validateName(value); + this.value = value; + } + + private void validateName(final String value) { + if (StringChecker.isNullOrBlank(value)) { + throw new ArtistException.NullOrEmptyNameException(); + } + if (value.length() > NAME_MAXIMUM_LENGTH) { + throw new ArtistException.TooLongNameException( + Map.of("Singer", value) + ); + } + } + + public boolean startsWithIgnoringCaseAndWhiteSpace(final String keyword) { + final String targetKeyword = toLowerCaseRemovingWhiteSpace(keyword); + if (StringChecker.isNullOrBlank(targetKeyword)) { + return false; + } + + return toLowerCaseRemovingWhiteSpace(value) + .startsWith(toLowerCaseRemovingWhiteSpace(keyword)); + } + + private String toLowerCaseRemovingWhiteSpace(final String word) { + return removeAllWhiteSpace(word).toLowerCase(); + } + + public boolean endsWithIgnoringCaseAndWhiteSpace(final String keyword) { + final String targetKeyword = toLowerCaseRemovingWhiteSpace(keyword); + if (StringChecker.isNullOrBlank(targetKeyword)) { + return false; + } + + return toLowerCaseRemovingWhiteSpace(value) + .endsWith(toLowerCaseRemovingWhiteSpace(keyword)); + } + + private String removeAllWhiteSpace(final String word) { + return word.replaceAll(BLANK, ""); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java new file mode 100644 index 000000000..6fb63223f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java @@ -0,0 +1,71 @@ +package shook.shook.song.domain; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "artist_synonym") +@Entity +public class ArtistSynonym { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) + private Artist artist; + + @Embedded + private Synonym synonym; + + public ArtistSynonym(final Artist artist, final Synonym synonym) { + this.artist = artist; + this.synonym = synonym; + } + + public boolean startsWith(final String keyword) { + return synonym.startsWithIgnoringCaseAndWhiteSpace(keyword); + } + + public boolean endsWith(final String keyword) { + return synonym.endsWithIgnoringCaseAndWhiteSpace(keyword); + } + + public String getArtistName() { + return artist.getArtistName(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ArtistSynonym artistSynonym = (ArtistSynonym) o; + if (Objects.isNull(artistSynonym.id) || Objects.isNull(this.id)) { + return false; + } + return Objects.equals(id, artistSynonym.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonyms.java b/backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonyms.java new file mode 100644 index 000000000..3553d809b --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonyms.java @@ -0,0 +1,72 @@ +package shook.shook.song.domain; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiPredicate; +import java.util.stream.Stream; +import org.springframework.stereotype.Repository; + +@Repository +public class InMemoryArtistSynonyms { + + private Map artistsBySynonym = new HashMap<>(); + + public void initialize(final Map artistsBySynonym) { + this.artistsBySynonym = new HashMap<>(artistsBySynonym); + } + + public List findAllArtistsHavingSynonymStartsOrEndsWith(final String keyword) { + return Stream.concat( + findAllArtistsHavingSynonymStartsWith(keyword).stream(), + findAllArtistsHavingSynonymEndsWith(keyword).stream()) + .distinct() + .toList(); + } + + public List findAllArtistsHavingSynonymStartsWith(final String keyword) { + return filterBySynonymCondition(keyword, ArtistSynonym::startsWith); + } + + private List filterBySynonymCondition(final String keyword, + final BiPredicate filter) { + return artistsBySynonym.entrySet().stream() + .filter(entry -> filter.test(entry.getKey(), keyword)) + .map(Entry::getValue) + .distinct() + .toList(); + } + + private List findAllArtistsHavingSynonymEndsWith(final String keyword) { + return filterBySynonymCondition(keyword, ArtistSynonym::endsWith); + } + + public List findAllArtistsNameStartsOrEndsWith(final String keyword) { + return Stream.concat( + findAllArtistsNameStartsWith(keyword).stream(), + findAllArtistsNameEndsWith(keyword).stream()) + .distinct() + .toList(); + } + + public List findAllArtistsNameStartsWith(final String keyword) { + return filterByNameCondition(keyword, Artist::nameStartsWith); + } + + private List filterByNameCondition(final String keyword, + final BiPredicate filter) { + return artistsBySynonym.values().stream() + .filter(artist -> filter.test(artist, keyword)) + .distinct() + .toList(); + } + + private List findAllArtistsNameEndsWith(final String keyword) { + return filterByNameCondition(keyword, Artist::nameEndsWith); + } + + public Map getArtistsBySynonym() { + return new HashMap<>(artistsBySynonym); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonymsGenerator.java b/backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonymsGenerator.java new file mode 100644 index 000000000..f3e2f41ea --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonymsGenerator.java @@ -0,0 +1,32 @@ +package shook.shook.song.domain; + +import jakarta.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import shook.shook.song.domain.repository.ArtistSynonymRepository; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +@Component +public class InMemoryArtistSynonymsGenerator { + + private final InMemoryArtistSynonyms artistSynonyms; + private final ArtistSynonymRepository artistSynonymRepository; + + @PostConstruct + public void initialize() { + log.info("Initialize ArtistWithSynonym"); + final List synonymsWithArtist = artistSynonymRepository.findAll(); + final Map artistsBySynonym = new HashMap<>(); + for (final ArtistSynonym artistSynonym : synonymsWithArtist) { + artistsBySynonym.put(artistSynonym, artistSynonym.getArtist()); + } + artistSynonyms.initialize(artistsBySynonym); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/ProfileImageUrl.java b/backend/src/main/java/shook/shook/song/domain/ProfileImageUrl.java new file mode 100644 index 000000000..3b826d909 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ProfileImageUrl.java @@ -0,0 +1,39 @@ +package shook.shook.song.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.util.Map; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import shook.shook.song.exception.ArtistException; +import shook.shook.util.StringChecker; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode +@Embeddable +public class ProfileImageUrl { + + private static final int MAXIMUM_LENGTH = 65_536; + + @Column(name = "profile_image_url", columnDefinition = "text", nullable = false) + private String value; + + public ProfileImageUrl(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (StringChecker.isNullOrBlank(value)) { + throw new ArtistException.NullOrEmptyProfileUrlException(); + } + if (value.length() > MAXIMUM_LENGTH) { + throw new ArtistException.TooLongProfileUrlException( + Map.of("ArtistProfileImageUrl", value) + ); + } + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/Singer.java b/backend/src/main/java/shook/shook/song/domain/Singer.java index 87904a040..0f3bc4091 100644 --- a/backend/src/main/java/shook/shook/song/domain/Singer.java +++ b/backend/src/main/java/shook/shook/song/domain/Singer.java @@ -7,7 +7,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import shook.shook.song.exception.SongException; +import shook.shook.song.exception.ArtistException; import shook.shook.util.StringChecker; @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -15,6 +15,7 @@ @EqualsAndHashCode @Embeddable public class Singer { + // TODO: 2023-10-09 데이터 옮긴 후 Song에 있는 해당 컬럼을,, 날려야 하나..? private static final int NAME_MAXIMUM_LENGTH = 50; @@ -28,10 +29,10 @@ public Singer(final String name) { private void validateName(final String name) { if (StringChecker.isNullOrBlank(name)) { - throw new SongException.NullOrEmptySingerNameException(); + throw new ArtistException.NullOrEmptyNameException(); } if (name.length() > NAME_MAXIMUM_LENGTH) { - throw new SongException.TooLongSingerNameException( + throw new ArtistException.TooLongNameException( Map.of("Singer", name) ); } diff --git a/backend/src/main/java/shook/shook/song/domain/Song.java b/backend/src/main/java/shook/shook/song/domain/Song.java index d48851fc6..a7c5038ff 100644 --- a/backend/src/main/java/shook/shook/song/domain/Song.java +++ b/backend/src/main/java/shook/shook/song/domain/Song.java @@ -5,9 +5,13 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import java.time.LocalDateTime; @@ -38,9 +42,12 @@ public class Song { @Embedded private AlbumCoverUrl albumCoverUrl; - @Embedded - private Singer singer; + private KillingParts killingParts = new KillingParts(); + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) + private Artist artist; @Embedded private SongLength length; @@ -49,8 +56,6 @@ public class Song { @Enumerated(EnumType.STRING) private Genre genre; - @Embedded - private KillingParts killingParts = new KillingParts(); @Column(nullable = false, updatable = false) private LocalDateTime createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); @@ -65,7 +70,7 @@ private Song( final String title, final String videoId, final String imageUrl, - final String singer, + final Artist artist, final int length, final Genre genre, final KillingParts killingParts @@ -75,7 +80,7 @@ private Song( this.title = new SongTitle(title); this.videoId = new SongVideoId(videoId); this.albumCoverUrl = new AlbumCoverUrl(imageUrl); - this.singer = new Singer(singer); + this.artist = artist; this.length = new SongLength(length); this.genre = genre; killingParts.setSong(this); @@ -86,12 +91,12 @@ public Song( final String title, final String videoId, final String albumCoverUrl, - final String singer, + final Artist artist, final int length, final Genre genre, final KillingParts killingParts ) { - this(null, title, videoId, albumCoverUrl, singer, length, genre, killingParts); + this(null, title, videoId, albumCoverUrl, artist, length, genre, killingParts); } private void validate(final KillingParts killingParts) { @@ -120,8 +125,8 @@ public String getAlbumCoverUrl() { return albumCoverUrl.getValue(); } - public String getSinger() { - return singer.getName(); + public String getArtistName() { + return artist.getArtistName(); } public int getLength() { diff --git a/backend/src/main/java/shook/shook/song/domain/Synonym.java b/backend/src/main/java/shook/shook/song/domain/Synonym.java new file mode 100644 index 000000000..450e197a3 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/Synonym.java @@ -0,0 +1,68 @@ +package shook.shook.song.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.util.Map; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import shook.shook.song.exception.ArtistException; +import shook.shook.util.StringChecker; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode +@Embeddable +public class Synonym { + + private static final int MAXIMUM_LENGTH = 255; + private static final String BLANK = "\\s"; + + @Column(name = "synonym", nullable = false) + private String value; + + public Synonym(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (StringChecker.isNullOrBlank(value)) { + throw new ArtistException.NullOrEmptySynonymException(); + } + if (value.length() > MAXIMUM_LENGTH) { + throw new ArtistException.TooLongSynonymException( + Map.of("ArtistSynonym", value) + ); + } + } + + public boolean startsWithIgnoringCaseAndWhiteSpace(final String keyword) { + final String targetKeyword = toLowerCaseRemovingWhiteSpace(keyword); + if (StringChecker.isNullOrBlank(targetKeyword)) { + return false; + } + + return toLowerCaseRemovingWhiteSpace(value) + .startsWith(targetKeyword); + } + + public boolean endsWithIgnoringCaseAndWhiteSpace(final String keyword) { + final String targetKeyword = toLowerCaseRemovingWhiteSpace(keyword); + if (StringChecker.isNullOrBlank(targetKeyword)) { + return false; + } + + return toLowerCaseRemovingWhiteSpace(value) + .endsWith(toLowerCaseRemovingWhiteSpace(targetKeyword)); + } + + private String toLowerCaseRemovingWhiteSpace(final String word) { + return removeAllWhiteSpace(word).toLowerCase(); + } + + private String removeAllWhiteSpace(final String word) { + return word.replaceAll(BLANK, ""); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java new file mode 100644 index 000000000..ded9e3822 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java @@ -0,0 +1,10 @@ +package shook.shook.song.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import shook.shook.song.domain.Artist; + +@Repository +public interface ArtistRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/shook/shook/song/domain/repository/ArtistSynonymRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/ArtistSynonymRepository.java new file mode 100644 index 000000000..c8f60effc --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/repository/ArtistSynonymRepository.java @@ -0,0 +1,10 @@ +package shook.shook.song.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import shook.shook.song.domain.ArtistSynonym; + +@Repository +public interface ArtistSynonymRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java index fca5fd529..a0c09ce1f 100644 --- a/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java +++ b/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.Song; import shook.shook.song.domain.SongTitle; import shook.shook.song.domain.repository.dto.SongTotalLikeCountDto; @@ -47,4 +48,12 @@ List findSongsWithMoreLikeCountThanSongWithId( ); boolean existsSongByTitle(final SongTitle title); + + @Query("SELECT s AS song, SUM(COALESCE(kp.likeCount, 0)) AS totalLikeCount " + + "FROM Song s LEFT JOIN s.killingParts.killingParts kp " + + "WHERE s.artist = :artist " + + "GROUP BY s.id") + List findAllSongsWithTotalLikeCountByArtist( + @Param("artist") final Artist artist + ); } diff --git a/backend/src/main/java/shook/shook/song/exception/ArtistException.java b/backend/src/main/java/shook/shook/song/exception/ArtistException.java new file mode 100644 index 000000000..ee73f0188 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -0,0 +1,96 @@ +package shook.shook.song.exception; + +import java.util.Map; +import shook.shook.globalexception.CustomException; +import shook.shook.globalexception.ErrorCode; + +public class ArtistException extends CustomException { + + public ArtistException(final ErrorCode errorCode) { + super(errorCode); + } + + public ArtistException( + final ErrorCode errorCode, + final Map inputValuesByProperty + ) { + super(errorCode, inputValuesByProperty); + } + + public static class NullOrEmptyProfileUrlException extends ArtistException { + + public NullOrEmptyProfileUrlException() { + super(ErrorCode.EMPTY_ARTIST_PROFILE_URL); + } + + public NullOrEmptyProfileUrlException(final Map inputValuesByProperty) { + super(ErrorCode.EMPTY_ARTIST_PROFILE_URL, inputValuesByProperty); + } + } + + public static class TooLongProfileUrlException extends ArtistException { + + public TooLongProfileUrlException() { + super(ErrorCode.TOO_LONG_ARTIST_PROFILE_URL); + } + + public TooLongProfileUrlException(final Map inputValuesByProperty) { + super(ErrorCode.TOO_LONG_ARTIST_PROFILE_URL, inputValuesByProperty); + } + } + + public static class NullOrEmptyNameException extends ArtistException { + + public NullOrEmptyNameException() { + super(ErrorCode.EMPTY_SINGER_NAME); + } + + public NullOrEmptyNameException(final Map inputValuesByProperty) { + super(ErrorCode.EMPTY_SINGER_NAME, inputValuesByProperty); + } + } + + public static class TooLongNameException extends ArtistException { + + public TooLongNameException() { + super(ErrorCode.TOO_LONG_SINGER_NAME); + } + + public TooLongNameException(final Map inputValuesByProperty) { + super(ErrorCode.TOO_LONG_SINGER_NAME, inputValuesByProperty); + } + } + + public static class NullOrEmptySynonymException extends ArtistException { + + public NullOrEmptySynonymException() { + super(ErrorCode.EMPTY_ARTIST_SYNONYM); + } + + public NullOrEmptySynonymException(final Map inputValuesByProperty) { + super(ErrorCode.EMPTY_ARTIST_SYNONYM, inputValuesByProperty); + } + } + + public static class TooLongSynonymException extends ArtistException { + + public TooLongSynonymException() { + super(ErrorCode.TOO_LONG_ARTIST_SYNONYM); + } + + public TooLongSynonymException(final Map inputValuesByProperty) { + super(ErrorCode.TOO_LONG_ARTIST_SYNONYM, inputValuesByProperty); + } + } + + public static class NotExistException extends ArtistException { + + public NotExistException() { + super(ErrorCode.ARTIST_NOT_EXIST); + } + + public NotExistException(final Map inputValuesByProperty) { + super(ErrorCode.ARTIST_NOT_EXIST, inputValuesByProperty); + } + } +} diff --git a/backend/src/main/java/shook/shook/song/exception/SongException.java b/backend/src/main/java/shook/shook/song/exception/SongException.java index 3bd9a8619..ca20be086 100644 --- a/backend/src/main/java/shook/shook/song/exception/SongException.java +++ b/backend/src/main/java/shook/shook/song/exception/SongException.java @@ -105,28 +105,6 @@ public TooLongImageUrlException(final Map inputValuesByProperty) } } - public static class NullOrEmptySingerNameException extends SongException { - - public NullOrEmptySingerNameException() { - super(ErrorCode.EMPTY_SINGER_NAME); - } - - public NullOrEmptySingerNameException(final Map inputValuesByProperty) { - super(ErrorCode.EMPTY_SINGER_NAME, inputValuesByProperty); - } - } - - public static class TooLongSingerNameException extends SongException { - - public TooLongSingerNameException() { - super(ErrorCode.TOO_LONG_SINGER_NAME); - } - - public TooLongSingerNameException(final Map inputValuesByProperty) { - super(ErrorCode.TOO_LONG_SINGER_NAME, inputValuesByProperty); - } - } - public static class SongAlreadyExistException extends SongException { public SongAlreadyExistException() { diff --git a/backend/src/main/java/shook/shook/song/ui/ArtistController.java b/backend/src/main/java/shook/shook/song/ui/ArtistController.java new file mode 100644 index 000000000..29fe06893 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/ArtistController.java @@ -0,0 +1,33 @@ +package shook.shook.song.ui; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shook.shook.song.application.ArtistSearchService; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.ui.openapi.ArtistApi; + +@RequiredArgsConstructor +@RequestMapping("/singers") +@RestController +public class ArtistController implements ArtistApi { + + private final ArtistSearchService artistSearchService; + + @GetMapping("/{artist_id}") + public ResponseEntity searchSongsByArtist( + @PathVariable(name = "artist_id") final Long artistId) { + return ResponseEntity.ok(artistSearchService.searchAllSongsByArtist(artistId)); + } + + @PutMapping("/synonyms") + public ResponseEntity updateArtistSynonym() { + artistSearchService.updateArtistSynonymFromDatabase(); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/shook/shook/song/ui/SearchController.java b/backend/src/main/java/shook/shook/song/ui/SearchController.java new file mode 100644 index 000000000..86951f123 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/SearchController.java @@ -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.ArtistSearchService; +import shook.shook.song.ui.openapi.SearchApi; + +@RequiredArgsConstructor +@RequestMapping("/search") +@RestController +public class SearchController implements SearchApi { + + private final ArtistSearchService artistSearchService; + + @GetMapping + public ResponseEntity> search(@RequestParam(name = "type") final List types, + @RequestParam(name = "keyword") final String keyword) { + if (types.containsAll(List.of("song", "singer"))) { + return ResponseEntity.ok(artistSearchService.searchArtistsAndTopSongsByKeyword(keyword)); + } + return ResponseEntity.ok(artistSearchService.searchArtistsByKeyword(keyword)); + } + // TODO: 2023-10-19 리팩터링: 검색 타입 enum 생성 +} diff --git a/backend/src/main/java/shook/shook/song/ui/openapi/ArtistApi.java b/backend/src/main/java/shook/shook/song/ui/openapi/ArtistApi.java new file mode 100644 index 000000000..df7494a68 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/openapi/ArtistApi.java @@ -0,0 +1,32 @@ +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 org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; + +@Tag(name = "Artist", description = "가수 API") +public interface ArtistApi { + + @Operation( + summary = "특정 가수의 모든 노래 조회", + description = "가수의 모든 노래를 좋아요 순으로 조회한다." + ) + @ApiResponse( + responseCode = "200", + description = "가수 정보, 노래 목록 검색 성공" + ) + @Parameter( + name = "artist_id", + description = "가수 id", + required = true + ) + @GetMapping("/{artist_id}") + ResponseEntity searchSongsByArtist( + @PathVariable(name = "artist_id") final Long artistId + ); +} diff --git a/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java b/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java new file mode 100644 index 000000000..b558075cd --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java @@ -0,0 +1,38 @@ +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; + +@Tag(name = "Search", description = "검색 API") +public interface SearchApi { + + @Operation( + summary = "가수, 또는 가수와 노래 조회", + description = "가수를 가나다 순으로 조회한다." + ) + @ApiResponse( + responseCode = "200", + description = "가수 정보, (노래 목록) 검색 성공" + ) + @Parameter( + name = "type", + description = "검색 타입, singer&song OR singer", + required = true + ) + @Parameter( + name = "keyword", + description = "검색 키워드", + required = true + ) + @GetMapping("") + ResponseEntity> search( + @RequestParam(name = "type") final List types, + @RequestParam(name = "keyword") final String keyword + ); +} diff --git a/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java b/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java index 1036c139f..6fca3e57a 100644 --- a/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java +++ b/backend/src/main/java/shook/shook/voting_song/application/VotingSongService.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.voting_song.application.dto.VotingSongRegisterRequest; import shook.shook.voting_song.application.dto.VotingSongResponse; import shook.shook.voting_song.application.dto.VotingSongSwipeResponse; @@ -22,10 +23,13 @@ public class VotingSongService { private static final int AFTER_SONG_COUNT = 4; private final VotingSongRepository votingSongRepository; + private final ArtistRepository artistRepository; @Transactional public void register(final VotingSongRegisterRequest request) { - votingSongRepository.save(request.getVotingSong()); + final VotingSong votingSong = request.getVotingSong(); + artistRepository.save(votingSong.getArtist()); + votingSongRepository.save(votingSong); } public VotingSongSwipeResponse findAllForSwipeById(final Long id) { diff --git a/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongRegisterRequest.java b/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongRegisterRequest.java index 315c94675..bd99a72ee 100644 --- a/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongRegisterRequest.java +++ b/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongRegisterRequest.java @@ -8,6 +8,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.song.domain.Artist; import shook.shook.voting_song.domain.VotingSong; @Schema(description = "파트 수집 중인 노래 등록 요청") @@ -30,7 +31,11 @@ public class VotingSongRegisterRequest { @Schema(description = "가수 이름", example = "가수") @NotBlank - private String singer; + private String artistName; + + @Schema(description = "가수 프로필 이미지", example = "https://image.com/singer-profile.jpg") + @NotBlank + private String profileImageUrl; @Schema(description = "비디오 길이", example = "274") @NotNull @@ -38,6 +43,8 @@ public class VotingSongRegisterRequest { private Integer length; public VotingSong getVotingSong() { - return new VotingSong(title, videoId, imageUrl, singer, length); + final Artist artist = new Artist(profileImageUrl, artistName); + + return new VotingSong(title, videoId, imageUrl, artist, length); } } diff --git a/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongResponse.java b/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongResponse.java index 4be3569bc..603b95f0c 100644 --- a/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongResponse.java +++ b/backend/src/main/java/shook/shook/voting_song/application/dto/VotingSongResponse.java @@ -32,7 +32,7 @@ public static VotingSongResponse from(final VotingSong song) { return new VotingSongResponse( song.getId(), song.getTitle(), - song.getSinger(), + song.getArtistName(), song.getLength(), song.getVideoId(), song.getAlbumCoverUrl() diff --git a/backend/src/main/java/shook/shook/voting_song/domain/VotingSong.java b/backend/src/main/java/shook/shook/voting_song/domain/VotingSong.java index 10bc317db..0ce94174a 100644 --- a/backend/src/main/java/shook/shook/voting_song/domain/VotingSong.java +++ b/backend/src/main/java/shook/shook/voting_song/domain/VotingSong.java @@ -3,9 +3,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import java.time.LocalDateTime; @@ -17,7 +21,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import shook.shook.song.domain.AlbumCoverUrl; -import shook.shook.song.domain.Singer; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.SongLength; import shook.shook.song.domain.SongTitle; import shook.shook.song.domain.SongVideoId; @@ -42,8 +46,9 @@ public class VotingSong { @Embedded private AlbumCoverUrl albumCoverUrl; - @Embedded - private Singer singer; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) + private Artist artist; @Embedded private SongLength length; @@ -58,14 +63,14 @@ public VotingSong( final String title, final String videoId, final String albumCoverUrl, - final String singer, + final Artist artist, final int length ) { this.id = null; this.title = new SongTitle(title); this.videoId = new SongVideoId(videoId); this.albumCoverUrl = new AlbumCoverUrl(albumCoverUrl); - this.singer = new Singer(singer); + this.artist = artist; this.length = new SongLength(length); } @@ -110,8 +115,8 @@ public String getAlbumCoverUrl() { return albumCoverUrl.getValue(); } - public String getSinger() { - return singer.getName(); + public String getArtistName() { + return artist.getArtistName(); } public int getLength() { diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml index 84acf5b02..fc22e8b16 100644 --- a/backend/src/main/resources/application-test.yml +++ b/backend/src/main/resources/application-test.yml @@ -50,4 +50,4 @@ schedules: in-memory-token: cron: "0/1 * * * * *" in-memory-song: - cron: "0/1 * * * * *" # 1초 + cron: "0 0/1 * * * *" # 1분 diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index b24f7282b..56b78124d 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -1,28 +1,46 @@ +-- 사용 불가한 data.sql + TRUNCATE TABLE song; TRUNCATE TABLE voting_song; +TRUNCATE TABLE artist; + +INSERT INTO artist (name, profile_image_url, created_at) values ('NewJeans', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); +INSERT INTO artist (name, profile_image_url, created_at) values ('AKMU (악뮤)', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); +INSERT INTO artist (name, profile_image_url, created_at) values ('정국', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); + +INSERT INTO artist_synonym (artist_id, synonym) values (1, '뉴진스'); +INSERT INTO artist_synonym (artist_id, synonym) values (2, '악동뮤지션'); +INSERT INTO artist_synonym (artist_id, synonym) values (2, '악뮤'); +INSERT INTO artist_synonym (artist_id, synonym) values (3, 'Jung Kook'); +INSERT INTO artist_synonym (artist_id, synonym) values (3, '전정국'); + +INSERT INTO artist (name, profile_image_url, created_at) values ('아이미', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); +INSERT INTO artist (name, profile_image_url, created_at) values ('아이유', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); +INSERT INTO artist_synonym (artist_id, synonym) values (4, 'I ME'); +INSERT INTO artist_synonym (artist_id, synonym) values (5, 'IU'); -insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) -values ('N.Y.C.T', 'NCT U', 241, '8umUXHLGl3o', +insert into voting_song (title, artist_id, length, video_id, album_cover_url, created_at) +values ('N.Y.C.T', 1, 241, '8umUXHLGl3o', 'https://cdnimg.melon.co.kr/cm2/album/images/113/22/590/11322590_20230907111726_500.jpg?3d8bcc03a4900fdba3f199390f432b24/melon/resize/140/quality/80/optimize', now()); -insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) -values ('Slow Dancing', 'V', 190, 'eI0iTRS0Ha8', +insert into voting_song (title, artist_id, length, video_id, album_cover_url, created_at) +values ('Slow Dancing', 2, 190, 'eI0iTRS0Ha8', 'https://cdnimg.melon.co.kr/cm2/album/images/113/03/638/11303638_20230811103847_500.jpg?92b308988cd1521e8bd4d9c2f56768ed/melon/resize/140/quality/80/optimize', now()); -insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) -values ('LET''S DANCE', '이채연', 222, 'kQFLWdjk_8s', +insert into voting_song (title, artist_id, length, video_id, album_cover_url, created_at) +values ('LET''S DANCE', 3, 222, 'kQFLWdjk_8s', 'https://cdnimg.melon.co.kr/cm2/album/images/113/19/933/11319933_20230905152508_500.jpg?2bc0bb896e182ebb6ab11119b40657bc/melon/resize/140/quality/80/optimize', now()); -insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) -values ('Smoke (Prod. Dynamicduo, Padi)', '다이나믹 듀오, 이영지', 210, 'ZwXzaqzRVi4', +insert into voting_song (title, artist_id, length, video_id, album_cover_url, created_at) +values ('Smoke (Prod. Dynamicduo, Padi)', 2, 210, 'ZwXzaqzRVi4', 'https://cdnimg.melon.co.kr/cm2/album/images/113/15/612/11315612_20230905120657_500.jpg?e9f1ae79ad72f3749b9678e7ebd90027/melon/resize/140/quality/80/optimize', now()); -insert into voting_song (title, singer, length, video_id, album_cover_url, created_at) -values ('뭣 같아', 'BOYNEXTDOOR', 180, '97_-_WugRFA', +insert into voting_song (title, artist_id, length, video_id, album_cover_url, created_at) +values ('뭣 같아', 1, 180, '97_-_WugRFA', 'https://cdnimg.melon.co.kr/cm2/album/images/113/19/182/11319182_20230904102829_500.jpg?6555bb763ac1707683f5d05c0ab1b496/melon/resize/140/quality/80/optimize', now()); @@ -33,8 +51,8 @@ VALUES (10, 10, 1, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 1, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('해요 (2022)', '#안녕', 238, 'P6gV_t70KAk', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('해요 (2022)', 1, 238, 'P6gV_t70KAk', 'https://cdnimg.melon.co.kr/cm2/album/images/109/75/276/10975276_20220603165713_500.jpg?690c69f1d7581bed46767533175728ff/melon/resize/282/quality/80/optimize', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -44,8 +62,8 @@ VALUES (10, 10, 2, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 2, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('TOMBOY', '(여자)아이들', 174, '0wezH4MAncY', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('TOMBOY', 2, 174, '0wezH4MAncY', 'https://cdnimg.melon.co.kr/cm2/album/images/108/90/384/10890384_20220314111504_500.jpg?4b9dba7aeba43a4e0042eedb6b9865c1/melon/resize/282/quality/80/optimize', 'ROCK_METAL', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -55,8 +73,8 @@ VALUES (10, 10, 3, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 3, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('다정히 내 이름을 부르면', '경서예지, 전건호', 263, 'b_6EfFZyBxY', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('다정히 내 이름을 부르면', 2, 263, 'b_6EfFZyBxY', 'https://cdnimg.melon.co.kr/cm2/album/images/106/10/525/10610525_20210518143433_500.jpg?e8c5aa44ff6608c13fa48eb6a20e81af/melon/resize/282/quality/80/optimize', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -66,8 +84,8 @@ VALUES (10, 10, 4, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 4, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('That''s Hilarious', 'Charlie Puth', 146, 'F3KMndbOhIcㅍ', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('That''s Hilarious', 3, 146, 'F3KMndbOhIcㅍ', 'https://cdnimg.melon.co.kr/cm2/album/images/108/44/485/10844485_20221006154824_500.jpg?b752b5ed8fad66b79e2705840630dd94/melon/resize/282/quality/80/optimize', 'POP', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -77,8 +95,8 @@ VALUES (10, 10, 5, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 5, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Heaven(2023)', '임재현', 279, 'fPLXgfcyoMc', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Heaven(2023)', 2, 279, 'fPLXgfcyoMc', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -88,8 +106,8 @@ VALUES (10, 10, 6, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 6, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('당신을 만나', '김호중, 송가인', 238, 'kn_j1Ipw4DM', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('당신을 만나', 1, 238, 'kn_j1Ipw4DM', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', 'TROT', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -99,8 +117,8 @@ VALUES (10, 10, 7, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 7, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('잘 지내자, 우리 (여름날 우리 X 로이킴)', '로이킴', 258, 'MbSAeRQl0Xw', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('잘 지내자, 우리 (여름날 우리 X 로이킴)', 2, 258, 'MbSAeRQl0Xw', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', 'INDIE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -110,8 +128,8 @@ VALUES (10, 10, 8, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 8, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('빛이 나는 너에게', '던 (DAWN)', 175, 'wkr3S0hIXLk', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('빛이 나는 너에게', 3, 175, 'wkr3S0hIXLk', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -121,8 +139,8 @@ VALUES (10, 10, 9, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 9, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('파랑 (Blue Wave)', 'NCT DREAM', 189, 'NhgoqtRhb4g', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('파랑 (Blue Wave)', 3, 189, 'NhgoqtRhb4g', 'https://cdnimg.melon.co.kr/cm2/album/images/111/54/876/11154876_20230121133335_500.jpg?0ae26bb599c92ddd436282395563596e/melon/resize/282/quality/80/optimize', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -133,8 +151,8 @@ INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 10, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Ling Ling', '검정치마', 230, 'gjQwwWjxPaQ', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Ling Ling', 2, 230, 'gjQwwWjxPaQ', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/082/976/703/82976703_1663118461097_1_600x600.JPG/dims/resize/Q_80,0', 'INDIE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -144,8 +162,8 @@ VALUES (10, 10, 11, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 11, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('고백', '델리 스파이스 (Deli Spice)', 323, 'BYyVDi8BpZw', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('고백', 2, 323, 'BYyVDi8BpZw', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/015/027/552/15027552_1368610256849_1_600x600.JPG/dims/resize/Q_80,0', 'INDIE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -155,8 +173,8 @@ VALUES (10, 10, 12, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 12, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Polaroid', 'ENHYPEN', 184, 'vRdZVDWs3BI', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Polaroid', 1, 184, 'vRdZVDWs3BI', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/082/472/258/82472258_1641790812739_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -166,8 +184,8 @@ VALUES (10, 10, 13, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 13, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('사랑앓이', 'FTISLAND', 218, 'gnLwCb8Cz7I', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('사랑앓이', 2, 218, 'gnLwCb8Cz7I', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/049/974/430/49974430_1317964170310_1_600x600.JPG/dims/resize/Q_80,0', 'ROCK_METAL', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -177,8 +195,8 @@ VALUES (10, 10, 14, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 14, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('맞네', 'LUCY', 276, 'BRs0GGCT4bU', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('맞네', 3, 276, 'BRs0GGCT4bU', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/082/427/599/82427599_1638854125897_1_600x600.JPG/dims/resize/Q_80,0', 'ROCK_METAL', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -188,8 +206,8 @@ VALUES (10, 10, 15, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 15, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Madeleine Love', 'CHEEZE (치즈)', 218, 'EHTagN5HJKQ', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Madeleine Love', 2, 218, 'EHTagN5HJKQ', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/080/580/341/80580341_1431423374354_1_600x600.JPG/dims/resize/Q_80,0', 'INDIE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -199,8 +217,8 @@ VALUES (10, 10, 16, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 16, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('26', '윤하 (YOUNHA)', 199, 'eUqwF1-jjwQ', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('26', 1, 199, 'eUqwF1-jjwQ', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/341/257/81341257_1578293989428_1_600x600.JPG/dims/resize/Q_80,0', 'ROCK_METAL', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -210,8 +228,8 @@ VALUES (10, 10, 17, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 17, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('하늘 위로', 'IZ*ONE (아이즈원)', 192, 'P1jdwGsV4lk', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('하늘 위로', 3, 192, 'P1jdwGsV4lk', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -221,8 +239,8 @@ VALUES (10, 10, 18, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 18, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Super Shy', 'NewJeans', 200, 'ArmDp-zijuc', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Super Shy', 2, 200, 'ArmDp-zijuc', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -232,8 +250,8 @@ VALUES (10, 10, 19, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 19, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Seven (feat. Latto) - Clean Ver.', '정국', 186, 'UUSbUBYqU_8', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Seven (feat. Latto) - Clean Ver.', 3, 186, 'UUSbUBYqU_8', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'RHYTHM_AND_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -243,8 +261,8 @@ VALUES (10, 10, 20, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 20, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('퀸카 (Queencard)', '(여자)아이들', 162, 'VOcb6ZHxSjc', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('퀸카 (Queencard)', 2, 162, 'VOcb6ZHxSjc', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -254,8 +272,8 @@ VALUES (10, 10, 21, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 21, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('헤어지자 말해요', '박재정', 244, 'SrQzxD8UFdM', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('헤어지자 말해요', 1, 244, 'SrQzxD8UFdM', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -265,8 +283,8 @@ VALUES (10, 10, 22, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 22, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('I AM', 'IVE (아이브)', 208, 'cU0JrSAyy7o', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('I AM', 2, 208, 'cU0JrSAyy7o', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -276,8 +294,8 @@ VALUES (10, 10, 23, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 23, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('이브, 프시케 그리고 푸른 수염의 아내', 'LE SSERAFIM (르세라핌)', 186, +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('이브, 프시케 그리고 푸른 수염의 아내', 2, 186, 'Ii8L0qEvfC8', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); @@ -288,8 +306,8 @@ VALUES (10, 10, 24, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 24, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Spicy', 'aespa', 198, '1kfmWl3o8TE', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Spicy', 1, 198, '1kfmWl3o8TE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -299,8 +317,8 @@ VALUES (10, 10, 25, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 25, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Steal The Show (From "엘리멘탈")', 'Lauv', 194, 'kUMds6XKtfY', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Steal The Show (From "엘리멘탈")', 2, 194, 'kUMds6XKtfY', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'POP', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -310,8 +328,8 @@ VALUES (10, 10, 26, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 26, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('사랑은 늘 도망가', '임영웅', 273, 'pBEAzM2TRmE', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('사랑은 늘 도망가', 3, 273, 'pBEAzM2TRmE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -321,8 +339,8 @@ VALUES (10, 10, 27, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 27, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('ISTJ', 'NCT DREAM', 186, 'es60T3k-tyM', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('ISTJ', 3, 186, 'es60T3k-tyM', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -332,8 +350,8 @@ VALUES (10, 10, 28, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 28, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('모래 알갱이', '임영웅', 221, '3_wOZrzmQ1o', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('모래 알갱이', 2, 221, '3_wOZrzmQ1o', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -343,8 +361,8 @@ VALUES (10, 10, 29, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 29, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('UNFORGIVEN (feat. Nile Rodgers)', 'LE SSERAFIM (르세라핌)', 181, +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('UNFORGIVEN (feat. Nile Rodgers)', 2, 181, 'fzSDGXyGTjg', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); @@ -355,8 +373,8 @@ VALUES (10, 10, 30, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 30, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Kitsch', 'IVE (아이브)', 195, 'r572qh2__-U', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Kitsch', 1, 195, 'r572qh2__-U', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -366,8 +384,8 @@ VALUES (10, 10, 31, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 31, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('우리들의 블루스', '임영웅', 207, 'epz-aL5RaLQ', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('우리들의 블루스', 2, 207, 'epz-aL5RaLQ', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -377,8 +395,8 @@ VALUES (10, 10, 32, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 32, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Candy', 'NCT DREAM', 220, 'QuaVFoBLQeg', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Candy', 1, 220, 'QuaVFoBLQeg', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -388,8 +406,8 @@ VALUES (10, 10, 33, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 33, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Hype boy', 'NewJeans', 180, 'T--6HBX2K4g', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Hype boy', 1, 180, 'T--6HBX2K4g', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -399,8 +417,8 @@ VALUES (10, 10, 34, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 34, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('다시 만날 수 있을까', '임영웅', 275, 'VPDRLgfqfSs', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('다시 만날 수 있을까', 2, 275, 'VPDRLgfqfSs', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -410,8 +428,8 @@ VALUES (10, 10, 35, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 35, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Broken Melodies', 'NCT DREAM', 227, 'EPsh2192sTU', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Broken Melodies', 2, 227, 'EPsh2192sTU', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'ROCK_METAL', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -421,8 +439,8 @@ VALUES (10, 10, 36, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 36, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Still With You', '정국', 239, 'BksBNbTIoPE', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Still With You', 1, 239, 'BksBNbTIoPE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'RHYTHM_AND_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -432,8 +450,8 @@ VALUES (10, 10, 37, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 37, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('무지개', '임영웅', 198, 'o8e0Qd2H1qc', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('무지개', 2, 198, 'o8e0Qd2H1qc', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -443,8 +461,8 @@ VALUES (10, 10, 38, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 38, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Ditto', 'NewJeans', 187, 'haCpjUXIhrI', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Ditto', 3, 187, 'haCpjUXIhrI', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -454,8 +472,8 @@ VALUES (10, 10, 39, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 39, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('London Boy', '임영웅', 289, 'ZRDuScdwEbE', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('London Boy', 3, 289, 'ZRDuScdwEbE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -465,8 +483,8 @@ VALUES (10, 10, 40, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 40, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('이제 나만 믿어요', '임영웅', 274, 'y1KXYmMuZZA', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('이제 나만 믿어요', 2, 274, 'y1KXYmMuZZA', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'TROT', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -476,8 +494,8 @@ VALUES (10, 10, 41, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 41, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('아버지', '임영웅', 240, 'dbaiMJOnaB4', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('아버지', 3, 240, 'dbaiMJOnaB4', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'TROT', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -487,8 +505,8 @@ VALUES (10, 10, 42, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 42, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Polaroid', '임영웅', 209, 'PVDxs6GUXSI', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Polaroid', 2, 209, 'PVDxs6GUXSI', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -498,8 +516,8 @@ VALUES (10, 10, 43, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 43, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Dynamite', '방탄소년단', 198, 'KhZ5DCd7m6s', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Dynamite', 1, 198, 'KhZ5DCd7m6s', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'POP', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -509,8 +527,8 @@ VALUES (10, 10, 44, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 44, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('손오공', '세븐틴 (SEVENTEEN)', 200, 'tFPbzfU5XL4', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('손오공', 2, 200, 'tFPbzfU5XL4', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -520,8 +538,8 @@ VALUES (10, 10, 45, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 45, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('인생찬가', '임영웅', 235, 'cXHduPVrcDQ', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('인생찬가', 3, 235, 'cXHduPVrcDQ', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -531,8 +549,8 @@ VALUES (10, 10, 46, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 46, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('A bientot', '임영웅', 258, 'sZDDLUB8wQE', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('A bientot', 3, 258, 'sZDDLUB8wQE', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -542,8 +560,8 @@ VALUES (10, 10, 47, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 47, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('손이 참 곱던 그대', '임영웅', 197, 'OpZIaI-J0uk', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('손이 참 곱던 그대', 3, 197, 'OpZIaI-J0uk', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'BALLAD', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -553,8 +571,8 @@ VALUES (10, 10, 48, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 48, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('사랑해 진짜', '임영웅', 241, 'qkledxNCNfY', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('사랑해 진짜', 3, 241, 'qkledxNCNfY', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'FOLK_BLUES', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -564,8 +582,8 @@ VALUES (10, 10, 49, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 49, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('꽃', '지수', 174, '6zM48_rBFbY', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('꽃', 2, 174, '6zM48_rBFbY', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -575,8 +593,8 @@ VALUES (10, 10, 50, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 50, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('연애편지', '임영웅', 217, 'gSQFZvUuQ3s', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('연애편지', 3, 217, 'gSQFZvUuQ3s', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -586,8 +604,8 @@ VALUES (10, 10, 51, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 51, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('New jeans', 'New jeans', 109, 'G8GEpK7YDl4', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('New jeans', 3, 109, 'G8GEpK7YDl4', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -597,8 +615,8 @@ VALUES (10, 10, 52, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 52, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('OMG', 'New jeans', 215, 'jT0Lh-N3TSg', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('OMG', 3, 215, 'jT0Lh-N3TSg', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -608,8 +626,8 @@ VALUES (10, 10, 53, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 53, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Butter', '방탄소년단', 165, 'Uz0PppyT7Cc', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Butter', 2, 165, 'Uz0PppyT7Cc', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -619,8 +637,8 @@ VALUES (10, 10, 54, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 54, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('파랑 (Blue Wave)', 'NCT DREAM', 191, 'ZkLK4hUqqas', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('파랑 (Blue Wave)', 1, 191, 'ZkLK4hUqqas', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -630,8 +648,8 @@ VALUES (10, 10, 55, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 55, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Take Two', '방탄소년단', 230, '3UE-vpej_VI', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Take Two', 2, 230, '3UE-vpej_VI', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -641,8 +659,8 @@ VALUES (10, 10, 56, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 56, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Like We Just Met', 'NCT DREAM', 210, 'eA9pwL-8wJw', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Like We Just Met', 1, 210, 'eA9pwL-8wJw', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) @@ -652,8 +670,8 @@ VALUES (10, 10, 57, 5, now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) VALUES (100, 15, 57, 3, now()); -INSERT INTO song (title, singer, length, video_id, album_cover_url, genre, created_at) -VALUES ('Yogurt Shake', 'NCT DREAM', 218, 'IUs7tOzHVJw', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, genre, created_at) +VALUES ('Yogurt Shake', 1, 218, 'IUs7tOzHVJw', 'https://image.genie.co.kr/Y/IMAGE/IMG_ALBUM/081/179/198/81179198_1554106431207_1_600x600.JPG/dims/resize/Q_80,0', 'DANCE', now()); INSERT INTO killing_part(start_second, length, song_id, like_count, created_at) diff --git a/backend/src/main/resources/dev/schema.sql b/backend/src/main/resources/dev/schema.sql index 6fc289aee..9cc59c431 100644 --- a/backend/src/main/resources/dev/schema.sql +++ b/backend/src/main/resources/dev/schema.sql @@ -6,13 +6,15 @@ drop table if exists voting_song_part; drop table if exists voting_song; drop table if exists vote; drop table if exists member; +drop table if exists artist; drop table if exists member_part; +drop table if exists artist_synonym; create table if not exists song ( id bigint auto_increment, title varchar(100) not null, - singer varchar(50) not null, + artist_id bigint not null, length integer not null, video_id varchar(20) not null, album_cover_url text not null, @@ -60,9 +62,9 @@ create table if not exists voting_song ( id bigint auto_increment, title varchar(100) not null, - singer varchar(50) not null, length integer not null, video_id varchar(20) not null, + artist_id bigint not null, album_cover_url text not null, created_at timestamp(6) not null, primary key (id) @@ -94,6 +96,15 @@ create table if not exists member primary key (id) ); +create table if not exists artist +( + id bigint auto_increment, + name varchar(50) not null, + profile_image_url text not null, + created_at timestamp(6) not null, + primary key (id) +); + create table if not exists member_part ( id bigint auto_increment, @@ -104,3 +115,11 @@ create table if not exists member_part created_at timestamp(6) not null, primary key (id) ); + +create table if not exists artist_synonym +( + id bigint auto_increment, + artist_id bigint not null, + synonym varchar(255) not null, + primary key (id) +); diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index ac451ebd6..fd75458fe 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -105,3 +105,25 @@ alter table song 'FOLK_BLUES', 'POP', 'JAZZ', 'CLASSIC', 'J_POP', 'EDM', 'ETC')); alter table vote add column member_id bigint not null; + +create table if not exists artist +( + id bigint auto_increment, + name varchar(50) not null, + profile_image_url text not null, + created_at timestamp(6) not null, + primary key (id) +); + +ALTER TABLE song ADD COLUMN artist_id BIGINT NOT NULL; +ALTER TABLE song DROP COLUMN singer; +ALTER TABLE voting_song ADD COLUMN artist_id BIGINT NOT NULL; +ALTER TABLE voting_song DROP COLUMN singer; + +create table if not exists artist_synonym +( + id bigint auto_increment, + artist_id bigint not null, + synonym varchar(255) not null, + primary key (id) +); diff --git a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java index 3e2bd511d..da546c1fb 100644 --- a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java @@ -100,13 +100,21 @@ void success_reissue() { //when final ReissueAccessTokenResponse result = authService.reissueAccessTokenByRefreshToken( refreshToken, accessToken); + final Claims resultClaims = tokenProvider.parseClaims(accessToken); + final Object resultId = resultClaims.get("id"); + final Object resultNickname = resultClaims.get("nickname"); //then final String accessToken = tokenProvider.createAccessToken( savedMember.getId(), savedMember.getNickname()); - assertThat(result.getAccessToken()).isEqualTo(accessToken); + final Claims claims = tokenProvider.parseClaims(accessToken); + final Object expectedId = claims.get("id"); + final Object expectedNickname = claims.get("nickname"); + + assertThat(expectedId).isEqualTo(resultId); + assertThat(expectedNickname).isEqualTo(resultNickname); } @DisplayName("잘못된 refresh 토큰(secret Key가 다른)이 들어오면 예외를 던진다.") diff --git a/backend/src/test/java/shook/shook/exceptionhandler/ControllerAdviceTest.java b/backend/src/test/java/shook/shook/exceptionhandler/ControllerAdviceTest.java index 9960644a1..b07da9f02 100644 --- a/backend/src/test/java/shook/shook/exceptionhandler/ControllerAdviceTest.java +++ b/backend/src/test/java/shook/shook/exceptionhandler/ControllerAdviceTest.java @@ -15,6 +15,7 @@ import shook.shook.member.exception.MemberException; import shook.shook.part.exception.PartException; import shook.shook.song.application.SongService; +import shook.shook.song.exception.ArtistException; import shook.shook.song.exception.SongException; import shook.shook.song.exception.killingpart.KillingPartCommentException; import shook.shook.song.exception.killingpart.KillingPartException; @@ -96,9 +97,13 @@ private static Stream exceptionTestData() { new ExceptionTestData( new SongException.TooLongImageUrlException(), 400), new ExceptionTestData( - new SongException.NullOrEmptySingerNameException(), 400), + new ArtistException.NullOrEmptyNameException(), 400), new ExceptionTestData( - new SongException.TooLongSingerNameException(), 400), + new ArtistException.TooLongNameException(), 400), + new ExceptionTestData( + new ArtistException.NullOrEmptyProfileUrlException(), 400), + new ExceptionTestData( + new ArtistException.TooLongProfileUrlException(), 400), new ExceptionTestData(new PartException.StartLessThanZeroException(), 400), new ExceptionTestData(new PartException.StartOverSongLengthException(), 400), diff --git a/backend/src/test/java/shook/shook/member_part/domain/MemberPartTest.java b/backend/src/test/java/shook/shook/member_part/domain/MemberPartTest.java index 4b5cb8264..6edd2b7d6 100644 --- a/backend/src/test/java/shook/shook/member_part/domain/MemberPartTest.java +++ b/backend/src/test/java/shook/shook/member_part/domain/MemberPartTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import shook.shook.member.domain.Member; import shook.shook.member_part.exception.MemberPartException; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; import shook.shook.song.domain.Song; @@ -17,6 +18,7 @@ class MemberPartTest { private static Song SONG; private static Member MEMBER; + private static Artist ARTIST; @BeforeEach void setUp() { @@ -27,8 +29,8 @@ void setUp() { KillingPart.forSave(1, 10) ) ); - - SONG = new Song("title", "12345678901", "albumCover", "singer", 300, Genre.DANCE, killingParts); + ARTIST = new Artist("profile", "image"); + SONG = new Song("title", "12345678901", "albumCover", ARTIST, 300, Genre.DANCE, killingParts); MEMBER = new Member("shook@email.com", "shook"); } diff --git a/backend/src/test/java/shook/shook/member_part/domain/repository/MemberPartRepositoryTest.java b/backend/src/test/java/shook/shook/member_part/domain/repository/MemberPartRepositoryTest.java index 1c8ed572b..66191110f 100644 --- a/backend/src/test/java/shook/shook/member_part/domain/repository/MemberPartRepositoryTest.java +++ b/backend/src/test/java/shook/shook/member_part/domain/repository/MemberPartRepositoryTest.java @@ -12,10 +12,12 @@ import shook.shook.member.domain.repository.MemberRepository; import shook.shook.member_part.domain.MemberPart; import shook.shook.member_part.domain.repository.dto.SongMemberPartCreatedAtDto; +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; import shook.shook.support.UsingJpaTest; @@ -31,6 +33,9 @@ class MemberPartRepositoryTest extends UsingJpaTest { @Autowired private SongRepository songRepository; + @Autowired + private ArtistRepository artistRepository; + @DisplayName("멤버 아이디와 멤버 파트 아이디로 멤버 파트를 조회한다.") @Test void findByMemberIdAndId() { @@ -42,7 +47,7 @@ void findByMemberIdAndId() { // when final Optional optionalMember = memberPartRepository.findByMemberIdAndId(member.getId(), - memberPart.getId()); + memberPart.getId()); final MemberPart savedMemberPart = optionalMember.get(); // then @@ -56,8 +61,10 @@ private Song createNewSongWithKillingPartsAndSaveSong() { final KillingPart secondKillingPart = KillingPart.forSave(15, 5); final KillingPart thirdKillingPart = KillingPart.forSave(20, 5); + final Artist artist = new Artist("profile", "name"); + artistRepository.save(artist); final Song song = new Song( - "제목", "비디오ID는 11글자", "이미지URL", "가수", 180, Genre.from("댄스"), + "제목", "비디오ID는 11글자", "이미지URL", artist, 180, Genre.from("댄스"), new KillingParts(List.of(firstKillingPart, secondKillingPart, thirdKillingPart))); return songRepository.save(song); @@ -90,16 +97,16 @@ void findBySongIdIn() { // when final List memberParts = memberPartRepository.findByMemberAndSongIdIn(member, - List.of(firstSong.getId(), - secondSong.getId(), - thirdSong.getId())); + List.of(firstSong.getId(), + secondSong.getId(), + thirdSong.getId())); // then assertThat(memberParts).hasSize(3); assertThat(memberParts.stream() - .map(MemberPart::getSong) - .map(Song::getId) - .toList()).contains(firstSong.getId(), secondSong.getId(), thirdSong.getId()); + .map(MemberPart::getSong) + .map(Song::getId) + .toList()).contains(firstSong.getId(), secondSong.getId(), thirdSong.getId()); } @DisplayName("나의 파트를 조회한다.") diff --git a/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java new file mode 100644 index 000000000..f96d2bad5 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java @@ -0,0 +1,244 @@ +package shook.shook.song.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; +import shook.shook.member.domain.Member; +import shook.shook.member.domain.repository.MemberRepository; +import shook.shook.song.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistSynonym; +import shook.shook.song.domain.Genre; +import shook.shook.song.domain.InMemoryArtistSynonyms; +import shook.shook.song.domain.InMemoryArtistSynonymsGenerator; +import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.Song; +import shook.shook.song.domain.Synonym; +import shook.shook.song.domain.killingpart.KillingPart; +import shook.shook.song.domain.killingpart.KillingPartLike; +import shook.shook.song.domain.killingpart.repository.KillingPartLikeRepository; +import shook.shook.song.domain.killingpart.repository.KillingPartRepository; +import shook.shook.song.domain.repository.ArtistRepository; +import shook.shook.song.domain.repository.ArtistSynonymRepository; +import shook.shook.song.domain.repository.SongRepository; +import shook.shook.song.exception.ArtistException; +import shook.shook.support.UsingJpaTest; + +@SuppressWarnings("NonAsciiCharacters") +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +class ArtistSearchServiceTest extends UsingJpaTest { + + private ArtistSearchService artistSearchService; + private InMemoryArtistSynonyms artistSynonyms = new InMemoryArtistSynonyms(); + private InMemoryArtistSynonymsGenerator generator; + + @Autowired + private SongRepository songRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private KillingPartLikeRepository likeRepository; + + @Autowired + private KillingPartRepository killingPartRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ArtistSynonymRepository synonymRepository; + + @BeforeEach + void setUp() { + generator = new InMemoryArtistSynonymsGenerator(artistSynonyms, synonymRepository); + artistSearchService = new ArtistSearchService(artistSynonyms, artistRepository, + songRepository, generator); + final Song firstSong = songRepository.findById(1L).get(); + final Song secondSong = songRepository.findById(2L).get(); + final Song thirdSong = songRepository.findById(3L).get(); + final Song fourthSong = songRepository.findById(4L).get(); + final Member member = memberRepository.findById(1L).get(); + + addLikeToEachKillingParts(secondSong, member); + addLikeToEachKillingParts(thirdSong, member); + + initializeAllArtistsWithSynonyms(); + } + + private void addLikeToEachKillingParts(final Song song, final Member member) { + for (final KillingPart killingPart : song.getKillingParts()) { + final KillingPartLike like = new KillingPartLike(killingPart, member); + killingPart.like(like); + likeRepository.save(like); + } + } + + private void initializeAllArtistsWithSynonyms() { + final Artist newJeans = artistRepository.findById(1L).get(); + final Artist 가수 = artistRepository.findById(2L).get(); + final Artist 정국 = artistRepository.findById(3L).get(); + + artistSynonyms.initialize( + Map.of( + new ArtistSynonym(newJeans, new Synonym("뉴진스")), newJeans, + new ArtistSynonym(가수, new Synonym("인기가수")), 가수, + new ArtistSynonym(정국, new Synonym("방탄인기")), 정국 + ) + ); + } + + @DisplayName("동의어 또는 이름이 키워드로 시작하는 아티스트 목록을 가나다 순으로 정렬하여 검색한다.") + @Test + void searchArtistsByKeyword() { + // given + // when + final List artists = artistSearchService.searchArtistsByKeyword("인기"); + + // then + final Artist artist = artistRepository.findById(2L).get(); + + assertThat(artists).usingRecursiveComparison() + .isEqualTo(List.of(ArtistResponse.from(artist))); + } + + @DisplayName("아티스 목록을 모두 조회할 때 키워드가 비어있다면 빈 결과를 반환한다.") + @Test + void searchArtistsByKeyword_emptyKeyword() { + // given + // when + final List artists = artistSearchService.searchArtistsByKeyword(" "); + + // then + assertThat(artists).isEmpty(); + } + + @DisplayName("키워드로 시작하거나 끝나는 아티스트를 가나다 순으로 정렬, 해당 아티스트의 TOP 곡 목록을 모두 조회한다.") + @ParameterizedTest + @ValueSource(strings = {"국", "방탄"}) + void searchArtistsAndTopSongsByKeyword(final String keyword) { + // given + final Artist artist = artistRepository.findById(3L).get(); + final Song anotherSong1 = createNewSongWithArtist(artist); + final Song anotherSong2 = createNewSongWithArtist(artist); + final Song anotherSong3 = createNewSongWithArtist(artist); + saveSong(anotherSong1); + saveSong(anotherSong2); + saveSong(anotherSong3); + + addLikeToEachKillingParts(anotherSong1, memberRepository.findById(1L).get()); + // 예상 TOP3: anotherSong1, anotherSong3, anotherSong2 + saveAndClearEntityManager(); + + // when + final List artistsWithSong = artistSearchService.searchArtistsAndTopSongsByKeyword( + keyword); + + // then + final Artist foundArtist = artistRepository.findById(3L).get(); + final Song expectedFirstSong = songRepository.findById(anotherSong1.getId()).get(); + final Song expectedThirdSong = songRepository.findById(anotherSong3.getId()).get(); + final Song expectedSecondSong = songRepository.findById(anotherSong2.getId()).get(); + + assertThat(artistsWithSong).usingRecursiveComparison() + .isEqualTo(List.of(ArtistWithSongSearchResponse.of( + foundArtist, + 4, + List.of(expectedFirstSong, expectedThirdSong, expectedSecondSong)) + )); + } + + private void saveSong(final Song song) { + songRepository.save(song); + killingPartRepository.saveAll(song.getKillingParts()); + } + + private Song createNewSongWithArtist(final Artist artist) { + final KillingPart firstKillingPart = KillingPart.forSave(10, 5); + final KillingPart secondKillingPart = KillingPart.forSave(15, 5); + final KillingPart thirdKillingPart = KillingPart.forSave(20, 5); + + return new Song( + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, + Genre.from("댄스"), + new KillingParts(List.of(firstKillingPart, secondKillingPart, thirdKillingPart)) + ); + } + + @DisplayName("아티스트, 해당 아티스트의 TOP 곡 목록을 모두 조회할 때 키워드가 비어있다면 빈 결과를 반환한다.") + @Test + void searchArtistsAndTopSongsByKeyword_emptyKeyword() { + // given + // when + final List response = artistSearchService.searchArtistsAndTopSongsByKeyword( + " "); + + // then + assertThat(response).isEmpty(); + } + + @DisplayName("아티스트의 모든 곡 목록을 좋아요 순으로 정렬하여 조회한다.") + @Test + void searchAllSongsByArtist() { + // given + final Artist artist = artistRepository.findById(3L).get(); + final Song anotherSong1 = createNewSongWithArtist(artist); + final Song anotherSong2 = createNewSongWithArtist(artist); + final Song anotherSong3 = createNewSongWithArtist(artist); + saveSong(anotherSong1); + saveSong(anotherSong2); + saveSong(anotherSong3); + + addLikeToEachKillingParts(anotherSong1, memberRepository.findById(1L).get()); + // 예상 TOP3: anotherSong1, anotherSong3, anotherSong2, 4L Song + saveAndClearEntityManager(); + + // when + final ArtistWithSongSearchResponse artistSongsResponse = artistSearchService.searchAllSongsByArtist( + artist.getId()); + + // then + final Artist expectedArtist = artistRepository.findById(artist.getId()).get(); + final Song expectedFirstSong = songRepository.findById(anotherSong1.getId()).get(); + final Song expectedSecondSong = songRepository.findById(anotherSong3.getId()).get(); + final Song expectedThirdSong = songRepository.findById(anotherSong2.getId()).get(); + final Song expectedFourthSong = songRepository.findById(4L).get(); + + assertThat(artistSongsResponse).usingRecursiveComparison() + .isEqualTo(ArtistWithSongSearchResponse.of( + expectedArtist, + 4, + List.of( + expectedFirstSong, + expectedSecondSong, + expectedThirdSong, + expectedFourthSong) + )); + } + + @DisplayName("존재하지 않는 아티스트를 요청하면 예외가 발생한다.") + @Test + void searchAllSongsByArtist_artistNotExist() { + // given + final Long artistIdNotExist = Long.MAX_VALUE; + + // when, then + assertThatThrownBy(() -> artistSearchService.searchAllSongsByArtist(artistIdNotExist)) + .isInstanceOf(ArtistException.NotExistException.class); + } +} diff --git a/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java b/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java index 2a27104c1..cbe6ea259 100644 --- a/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java +++ b/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java @@ -1,5 +1,7 @@ package shook.shook.song.application; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -8,10 +10,6 @@ import org.springframework.test.context.jdbc.Sql; import shook.shook.song.domain.InMemorySongs; -import java.util.Collections; - -import static org.assertj.core.api.Assertions.assertThat; - @Sql(value = "classpath:/killingpart/initialize_killing_part_song.sql") @EnableScheduling @SpringBootTest @@ -33,16 +31,4 @@ void recreateCachedSong() { // then assertThat(inMemorySongs.getSongs()).hasSize(4); } - - @DisplayName("Scheduler 가 1초마다 실행된다.") - @Test - void schedule() throws InterruptedException { - // given - // when - inMemorySongs.recreate(Collections.emptyList()); - Thread.sleep(1000); - - // then - assertThat(inMemorySongs.getSongs()).hasSize(4); - } } diff --git a/backend/src/test/java/shook/shook/song/application/SongServiceTest.java b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java index dd74f0835..b4c981b4f 100644 --- a/backend/src/test/java/shook/shook/song/application/SongServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java @@ -29,6 +29,7 @@ import shook.shook.song.domain.killingpart.KillingPartLike; import shook.shook.song.domain.killingpart.repository.KillingPartLikeRepository; import shook.shook.song.domain.killingpart.repository.KillingPartRepository; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.song.domain.repository.SongRepository; import shook.shook.song.exception.SongException; import shook.shook.support.UsingJpaTest; @@ -51,6 +52,9 @@ class SongServiceTest extends UsingJpaTest { @Autowired private MemberPartRepository memberPartRepository; + @Autowired + private ArtistRepository artistRepository; + private final InMemorySongs inMemorySongs = new InMemorySongs(); private SongService songService; @@ -64,6 +68,7 @@ public void setUp() { memberRepository, memberPartRepository, inMemorySongs, + artistRepository, new SongDataExcelReader(" ", " ", " ") ); } @@ -73,7 +78,13 @@ public void setUp() { void register() { // given final SongWithKillingPartsRegisterRequest request = new SongWithKillingPartsRegisterRequest( - "title", "elevenVideo", "imageUrl", "singer", 300, "댄스", + "title", + "elevenVideo", + "imageUrl", + "singer", + "image", + 300, + "댄스", List.of( new KillingPartRegisterRequest(10, 5), new KillingPartRegisterRequest(15, 10), @@ -93,7 +104,7 @@ void register() { () -> assertThat(foundSong.getTitle()).isEqualTo("title"), () -> assertThat(foundSong.getVideoId()).isEqualTo("elevenVideo"), () -> assertThat(foundSong.getAlbumCoverUrl()).isEqualTo("imageUrl"), - () -> assertThat(foundSong.getSinger()).isEqualTo("singer"), + () -> assertThat(foundSong.getArtistName()).isEqualTo("singer"), () -> assertThat(foundSong.getCreatedAt()).isNotNull(), () -> assertThat(foundSong.getKillingParts()).hasSize(3) ); @@ -113,7 +124,7 @@ void findById_exist_login_member() { saveAndClearEntityManager(); final SongSwipeResponse response = songService.findSongByIdForFirstSwipe(song.getId(), - new MemberInfo(member.getId(), Authority.MEMBER)); + new MemberInfo(member.getId(), Authority.MEMBER)); //then assertAll( @@ -121,19 +132,19 @@ void findById_exist_login_member() { () -> assertThat(response.getNextSongs()).isEmpty(), () -> assertThat(response.getCurrentSong().getKillingParts().get(0)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(0).getId()) + song.getLikeCountSortedKillingParts().get(0).getId()) .hasFieldOrPropertyWithValue("rank", 1) .hasFieldOrPropertyWithValue("likeStatus", true), () -> assertThat(response.getCurrentSong().getKillingParts().get(1)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(1).getId()) + song.getLikeCountSortedKillingParts().get(1).getId()) .hasFieldOrPropertyWithValue("rank", 2) .hasFieldOrPropertyWithValue("likeStatus", true), () -> assertThat(response.getCurrentSong().getKillingParts().get(2)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(2).getId()) + song.getLikeCountSortedKillingParts().get(2).getId()) .hasFieldOrPropertyWithValue("rank", 3) .hasFieldOrPropertyWithValue("likeStatus", true), () -> assertThat(response.getCurrentSong().getMemberPart().getId()).isNotNull() @@ -156,7 +167,7 @@ void findById_exist_not_login_member() { saveAndClearEntityManager(); final SongSwipeResponse response = songService.findSongByIdForFirstSwipe(song.getId(), - new MemberInfo(0L, Authority.ANONYMOUS)); + new MemberInfo(0L, Authority.ANONYMOUS)); //then assertAll( @@ -164,19 +175,19 @@ void findById_exist_not_login_member() { () -> assertThat(response.getNextSongs()).isEmpty(), () -> assertThat(response.getCurrentSong().getKillingParts().get(0)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(0).getId()) + song.getLikeCountSortedKillingParts().get(0).getId()) .hasFieldOrPropertyWithValue("rank", 1) .hasFieldOrPropertyWithValue("likeStatus", false), () -> assertThat(response.getCurrentSong().getKillingParts().get(1)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(1).getId()) + song.getLikeCountSortedKillingParts().get(1).getId()) .hasFieldOrPropertyWithValue("rank", 2) .hasFieldOrPropertyWithValue("likeStatus", false), () -> assertThat(response.getCurrentSong().getKillingParts().get(2)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(2).getId()) + song.getLikeCountSortedKillingParts().get(2).getId()) .hasFieldOrPropertyWithValue("rank", 3) .hasFieldOrPropertyWithValue("likeStatus", false), () -> assertThat(response.getCurrentSong().getMemberPart()).isNull() @@ -192,9 +203,9 @@ void findById_notExist() { //when //then assertThatThrownBy(() -> songService.findSongByIdForFirstSwipe( - 0L, - new MemberInfo(member.getId(), Authority.MEMBER) - ) + 0L, + new MemberInfo(member.getId(), Authority.MEMBER) + ) ).isInstanceOf(SongException.SongNotExistException.class); } @@ -241,7 +252,13 @@ void showHighLikedSongs() { private Song registerNewSong(final String title) { final SongWithKillingPartsRegisterRequest request = new SongWithKillingPartsRegisterRequest( - title, "elevenVideo", "imageUrl", "singer", 300, "댄스", + "title", + "elevenVideo", + "imageUrl", + "singer", + "image", + 300, + "댄스", List.of( new KillingPartRegisterRequest(10, 5), new KillingPartRegisterRequest(15, 10), @@ -299,7 +316,7 @@ void firstFindByMember() { // when final SongSwipeResponse result = songService.findSongByIdForFirstSwipe(fifthSong.getId(), - new MemberInfo(member.getId(), Authority.MEMBER)); + new MemberInfo(member.getId(), Authority.MEMBER)); // then assertAll( @@ -307,20 +324,20 @@ void firstFindByMember() { () -> assertThat(result.getPrevSongs()).hasSize(2), () -> assertThat(result.getNextSongs()).hasSize(2), () -> assertThat(result.getPrevSongs().stream() - .map(SongResponse::getId) - .toList()).usingRecursiveComparison().isEqualTo(List.of(4L, 3L)), + .map(SongResponse::getId) + .toList()).usingRecursiveComparison().isEqualTo(List.of(4L, 3L)), () -> assertThat(result.getNextSongs().stream() - .map(SongResponse::getId) - .toList()).usingRecursiveComparison().isEqualTo(List.of(2L, 1L)), + .map(SongResponse::getId) + .toList()).usingRecursiveComparison().isEqualTo(List.of(2L, 1L)), () -> assertThat(result.getCurrentSong().getMemberPart()).isNull(), () -> assertThat(result.getPrevSongs().stream() - .map(songResponse -> songResponse.getMemberPart().getId()) - .toList()) + .map(songResponse -> songResponse.getMemberPart().getId()) + .toList()) .usingRecursiveComparison() .isEqualTo(List.of(4L, 3L)), () -> assertThat(result.getNextSongs().stream() - .map(songResponse -> songResponse.getMemberPart().getId()) - .toList()) + .map(songResponse -> songResponse.getMemberPart().getId()) + .toList()) .usingRecursiveComparison() .isEqualTo(List.of(2L, 1L)) ); @@ -337,15 +354,15 @@ void firstFindByAnonymous() { // then assertThatThrownBy( () -> songService.findSongByIdForFirstSwipe(notExistSongId, - new MemberInfo(member.getId(), Authority.MEMBER))) + new MemberInfo(member.getId(), Authority.MEMBER))) .isInstanceOf(SongException.SongNotExistException.class); assertThatThrownBy( () -> songService.findSongByIdForBeforeSwipe(notExistSongId, - new MemberInfo(member.getId(), Authority.MEMBER))) + new MemberInfo(member.getId(), Authority.MEMBER))) .isInstanceOf(SongException.SongNotExistException.class); assertThatThrownBy( () -> songService.findSongByIdForAfterSwipe(notExistSongId, - new MemberInfo(member.getId(), Authority.MEMBER))) + new MemberInfo(member.getId(), Authority.MEMBER))) .isInstanceOf(SongException.SongNotExistException.class); } @@ -381,16 +398,16 @@ void findSongByIdForBeforeSwipe() { // when final List beforeResponses = songService.findSongByIdForBeforeSwipe(standardSong.getId(), - new MemberInfo(member.getId(), Authority.MEMBER)); + new MemberInfo(member.getId(), Authority.MEMBER)); // then assertThat(beforeResponses.stream() - .map(SongResponse::getId) - .toList()).usingRecursiveComparison().isEqualTo(List.of(2L, 4L, 1L, 5L)); + .map(SongResponse::getId) + .toList()).usingRecursiveComparison().isEqualTo(List.of(2L, 4L, 1L, 5L)); assertThat(beforeResponses.stream() - .map(SongResponse::getMemberPart) - .map(MemberPartResponse::getId) - .toList()).usingRecursiveComparison().isEqualTo(List.of(2L, 4L, 1L, 5L)); + .map(SongResponse::getMemberPart) + .map(MemberPartResponse::getId) + .toList()).usingRecursiveComparison().isEqualTo(List.of(2L, 4L, 1L, 5L)); } @DisplayName("이후 노래를 1. 좋아요 순 내림차순, 2. id 내림차순으로 조회한다.") @@ -425,16 +442,16 @@ void findSongByIdForAfterSwipe() { // when final List afterResponses = songService.findSongByIdForAfterSwipe(standardSong.getId(), - new MemberInfo(member.getId(), Authority.MEMBER)); + new MemberInfo(member.getId(), Authority.MEMBER)); // then assertThat(afterResponses.stream() - .map(SongResponse::getId) - .toList()).usingRecursiveComparison().isEqualTo(List.of(1L, 5L, 3L)); + .map(SongResponse::getId) + .toList()).usingRecursiveComparison().isEqualTo(List.of(1L, 5L, 3L)); assertThat(afterResponses.stream() - .map(SongResponse::getMemberPart) - .map(MemberPartResponse::getId) - .toList()).usingRecursiveComparison().isEqualTo(List.of(1L, 5L, 3L)); + .map(SongResponse::getMemberPart) + .map(MemberPartResponse::getId) + .toList()).usingRecursiveComparison().isEqualTo(List.of(1L, 5L, 3L)); } } @@ -473,7 +490,7 @@ void findSongsByGenre() { assertAll( () -> assertThat(response).hasSize(5), () -> assertThat(response.stream() - .map(HighLikedSongResponse::getId).toList()) + .map(HighLikedSongResponse::getId).toList()) .containsExactly(2L, 1L, 3L, 5L, 4L) ); } @@ -492,30 +509,30 @@ void findSongById() { // when final SongResponse response = songService.findSongById(song.getId(), - new MemberInfo(member.getId(), Authority.MEMBER)); + new MemberInfo(member.getId(), Authority.MEMBER)); // then assertAll( () -> assertThat(response.getId()).isEqualTo(song.getId()), () -> assertThat(response.getTitle()).isEqualTo(song.getTitle()), () -> assertThat(response.getAlbumCoverUrl()).isEqualTo(song.getAlbumCoverUrl()), - () -> assertThat(response.getSinger()).isEqualTo(song.getSinger()), + () -> assertThat(response.getSinger()).isEqualTo(song.getArtistName()), () -> assertThat(response.getKillingParts()).hasSize(3), () -> assertThat(response.getKillingParts().get(0)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(0).getId()) + song.getLikeCountSortedKillingParts().get(0).getId()) .hasFieldOrPropertyWithValue("rank", 1) .hasFieldOrPropertyWithValue("likeStatus", true), () -> assertThat(response.getKillingParts().get(1)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(1).getId()) + song.getLikeCountSortedKillingParts().get(1).getId()) .hasFieldOrPropertyWithValue("rank", 2) .hasFieldOrPropertyWithValue("likeStatus", true), () -> assertThat(response.getKillingParts().get(2)) .hasFieldOrPropertyWithValue("id", - song.getLikeCountSortedKillingParts().get(2).getId()) + song.getLikeCountSortedKillingParts().get(2).getId()) .hasFieldOrPropertyWithValue("rank", 3) .hasFieldOrPropertyWithValue("likeStatus", true), () -> assertThat(response.getMemberPart().getId()).isNotNull() diff --git a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java new file mode 100644 index 000000000..ffb551f61 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -0,0 +1,94 @@ +package shook.shook.song.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import shook.shook.song.exception.ArtistException; + +class ArtistNameTest { + + @DisplayName("가수 이름을 뜻하는 객체를 생성한다.") + @Test + void create_success() { + //given + //when + //then + assertDoesNotThrow(() -> new ArtistName("이름")); + } + + @DisplayName("가수 이름이 유효하지 않으면 예외를 던진다.") + @NullSource + @ParameterizedTest(name = "가수의 이름이 \"{0}\" 일 때") + @ValueSource(strings = {"", " "}) + void create_fail_lessThanOne(final String name) { + //given + //when + //then + assertThatThrownBy(() -> new ArtistName(name)) + .isInstanceOf(ArtistException.NullOrEmptyNameException.class); + } + + @DisplayName("가수 이름의 길이가 50을 넘을 경우 예외를 던진다.") + @Test + void create_fail_lengthOver50() { + //given + final String name = ".".repeat(51); + + //when + //then + assertThatThrownBy(() -> new ArtistName(name)) + .isInstanceOf(ArtistException.TooLongNameException.class); + } + + @DisplayName("입력값으로 시작하는 가수 이름이라면 true, 아니면 false 를 반환한다. (대소문자, 공백 제거)") + @ParameterizedTest + @CsvSource(value = {"Hi:true", "H i:true", "HiYou:true", " Hi:true", + "Hello:false"}, delimiter = ':') + void startsWithIgnoringCaseAndWhiteSpace(final String value, final boolean isSame) { + // given + final String keyword = "hi"; + final ArtistName artistName = new ArtistName(value); + + // when + final boolean result = artistName.startsWithIgnoringCaseAndWhiteSpace(keyword); + + // then + assertThat(result).isEqualTo(isSame); + } + + @DisplayName("입력값으로 끝나는 가수 이름이라면 true, 아니면 false 를 반환한다. (대소문자, 공백 제거)") + @ParameterizedTest + @CsvSource(value = {"HelloHi:true", "H i:true", " Hi :true", "Hello:false"}, delimiter = ':') + void endsWithIgnoringCaseAndWhiteSpace(final String value, final boolean isSame) { + // given + final String keyword = "hi"; + final ArtistName artistName = new ArtistName(value); + + // when + final boolean result = artistName.endsWithIgnoringCaseAndWhiteSpace(keyword); + + // then + assertThat(result).isEqualTo(isSame); + } + + @DisplayName("입력값이 비어있다면 false 가 반환된다.") + @Test + void ignoringCaseAndWhiteSpace_emptyValue() { + // given + final String keyword = " "; + final ArtistName artistName = new ArtistName("hi"); + + // when + final boolean result = artistName.endsWithIgnoringCaseAndWhiteSpace(keyword); + + // then + assertThat(result).isFalse(); + } +} diff --git a/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java new file mode 100644 index 000000000..f5b598d5c --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java @@ -0,0 +1,160 @@ +package shook.shook.song.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; +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.transaction.annotation.Transactional; +import shook.shook.song.domain.repository.ArtistRepository; +import shook.shook.song.domain.repository.ArtistSynonymRepository; + +@SpringBootTest +@Transactional +class InMemoryArtistSynonymsTest { + + private Artist artist1; + private Artist artist2; + private ArtistSynonym synonym1; + private ArtistSynonym synonym2; + + @Autowired + private ArtistSynonymRepository artistSynonymRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private InMemoryArtistSynonyms artistSynonyms; + + @Autowired + private InMemoryArtistSynonymsGenerator generator; + + @BeforeEach + void setUp() { + artist1 = new Artist("image", "name1"); + artist2 = new Artist("image", "name2"); + artistRepository.saveAll(List.of(artist1, artist2)); + + synonym1 = new ArtistSynonym(artist1, new Synonym("synonym1")); + synonym2 = new ArtistSynonym(artist2, new Synonym("synonym2")); + artistSynonymRepository.saveAll(List.of(synonym1, synonym2)); + } + + @DisplayName("InMemoryArtistSynonymsGenerator 에 의해 InMemoryArtistSynonyms 가 초기화된다.") + @Test + void generator_initialize() { + // given + // when + generator.initialize(); + + // then + assertThat(artistSynonyms.getArtistsBySynonym()).containsExactlyInAnyOrderEntriesOf( + Map.of(synonym1, artist1, synonym2, artist2) + ); + } + + @DisplayName("입력된 값으로 시작하거나 끝나는 동의어를 가진 아티스트를 모두 찾는다.") + @Test + void findAllArtistsHavingSynonymStartsOrEndsWith() { + // given + final Artist newArtist = new Artist("image", "newName"); + final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newTestSy")); + artistRepository.save(newArtist); + artistSynonymRepository.save(newSynonym); + + generator.initialize(); + + // when + final List result = artistSynonyms.findAllArtistsHavingSynonymStartsOrEndsWith( + "sy"); + + // then + assertThat(result).containsExactlyInAnyOrder(artist1, artist2, newArtist); + } + + @DisplayName("입력된 값으로 시작하는 동의어를 가진 아티스트를 모두 찾는다.") + @Test + void findAllArtistsHavingSynonymStartsWith() { + // given + final Artist newArtist = new Artist("image", "newName"); + final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newTestSy")); + artistRepository.save(newArtist); + artistSynonymRepository.save(newSynonym); + + generator.initialize(); + + // when + final List result = artistSynonyms.findAllArtistsHavingSynonymStartsWith( + "sy"); + + // then + assertThat(result).containsExactlyInAnyOrder(artist1, artist2); + } + + @DisplayName("동의어 검색 시, 입력된 값이 비어있다면 빈 결과가 반환된다.") + @Test + void findAllArtistsHavingSynonymStartsOrEndsWith_emptyInput() { + // given + // when + final List result = artistSynonyms.findAllArtistsHavingSynonymStartsOrEndsWith( + " "); + + // then + assertThat(result).isEmpty(); + } + + @DisplayName("입력된 값으로 시작하거나 끝나는 이름을 가진 아티스트를 모두 찾는다.") + @Test + void findAllArtistsNameStartsOrEndsWith() { + // given + final Artist newArtist = new Artist("image", "newName"); + final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newSynonym")); + artistRepository.save(newArtist); + artistSynonymRepository.save(newSynonym); + + generator.initialize(); + + // when + final List result = artistSynonyms.findAllArtistsNameStartsOrEndsWith( + "name"); + + // then + assertThat(result).containsExactlyInAnyOrder(artist1, artist2, newArtist); + } + + @DisplayName("입력된 값으로 시작하는 이름을 가진 아티스트를 모두 찾는다.") + @Test + void findAllArtistsNameStartsWith() { + // given + final Artist newArtist = new Artist("image", "newName"); + final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newSynonym")); + artistRepository.save(newArtist); + artistSynonymRepository.save(newSynonym); + + generator.initialize(); + + // when + final List result = artistSynonyms.findAllArtistsNameStartsWith( + "name"); + + // then + assertThat(result).containsExactlyInAnyOrder(artist1, artist2); + } + + @DisplayName("가수명 검색 시, 입력된 값이 비어있다면 빈 결과가 반환된다.") + @Test + void findAllArtistsNameStartsOrEndsWith_emptyInput() { + // given + // when + final List result = artistSynonyms.findAllArtistsNameStartsOrEndsWith( + " "); + + // then + assertThat(result).isEmpty(); + } +} diff --git a/backend/src/test/java/shook/shook/song/domain/ProfileImageUrlTest.java b/backend/src/test/java/shook/shook/song/domain/ProfileImageUrlTest.java new file mode 100644 index 000000000..1d78b9c89 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/ProfileImageUrlTest.java @@ -0,0 +1,46 @@ +package shook.shook.song.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import shook.shook.song.exception.ArtistException; + +class ProfileImageUrlTest { + + @DisplayName("ProfileImageUrl 을 생성한다.") + @Test + void create_success() { + // given + // when, then + assertDoesNotThrow(() -> new ProfileImageUrl("image")); + } + + @DisplayName("이미지 URL이 비어있으면 예외를 던진다.") + @NullSource + @ParameterizedTest(name = "이미지 URL이 \"{0}\" 일 때") + @ValueSource(strings = {"", " "}) + void create_fail_lessThanOne(final String value) { + //given + //when + //then + assertThatThrownBy(() -> new ProfileImageUrl(value)) + .isInstanceOf(ArtistException.NullOrEmptyProfileUrlException.class); + } + + @DisplayName("이미지 URL의 길이가 65_536을 넘을 경우 예외를 던진다.") + @Test + void create_fail_lengthOver65_536() { + //given + final String name = ".".repeat(65_537); + + //when + //then + assertThatThrownBy(() -> new ProfileImageUrl(name)) + .isInstanceOf(ArtistException.TooLongProfileUrlException.class); + } +} diff --git a/backend/src/test/java/shook/shook/song/domain/SingerTest.java b/backend/src/test/java/shook/shook/song/domain/SingerTest.java deleted file mode 100644 index 31fedd8b2..000000000 --- a/backend/src/test/java/shook/shook/song/domain/SingerTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package shook.shook.song.domain; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; -import shook.shook.song.exception.SongException; - -class SingerTest { - - @DisplayName("가수을 뜻하는 객체를 생성한다.") - @Test - void create_success() { - //given - //when - //then - Assertions.assertDoesNotThrow(() -> new Singer("이름")); - } - - @DisplayName("가수 이름이 유효하지 않으면 예외를 던진다.") - @NullSource - @ParameterizedTest(name = "가수의 이름이 \"{0}\" 일 때") - @ValueSource(strings = {"", " "}) - void create_fail_lessThanOne(final String name) { - //given - //when - //then - assertThatThrownBy(() -> new Singer(name)) - .isInstanceOf(SongException.NullOrEmptySingerNameException.class); - } - - @DisplayName("가수 이름의 길이가 100을 넘을 경우 예외를 던진다.") - @Test - void create_fail_lengthOver100() { - //given - final String name = ".".repeat(101); - - //when - //then - assertThatThrownBy(() -> new Singer(name)) - .isInstanceOf(SongException.TooLongSingerNameException.class); - } -} diff --git a/backend/src/test/java/shook/shook/song/domain/SongTest.java b/backend/src/test/java/shook/shook/song/domain/SongTest.java index 44f4f3e75..f3eb5c2b0 100644 --- a/backend/src/test/java/shook/shook/song/domain/SongTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SongTest.java @@ -17,8 +17,16 @@ void songCreate_nullKillingParts_fail() { // given // when, then assertThatThrownBy( - () -> new Song("title", "videoId", "imageUrl", "singer", 300, Genre.from("댄스"), null)) - .isInstanceOf(KillingPartsException.EmptyKillingPartsException.class); + () -> new Song( + "title", + "videoId", + "imageUrl", + new Artist("image", "name"), + 300, + Genre.from("댄스"), + null + ) + ).isInstanceOf(KillingPartsException.EmptyKillingPartsException.class); } @DisplayName("Song 의 KillingPart 시작 시간, 종료 시간이 지정된 재생 가능한 URL 을 반환한다.") @@ -31,9 +39,17 @@ void getPartVideoUrl() { final KillingParts killingParts = new KillingParts( List.of(killingPart1, killingPart2, killingPart3) ); - final Song song = new Song("title", "3rUPND6FG8A", "image_url", "singer", 230, - Genre.from("댄스"), - killingParts); + + final Artist artist = new Artist("image", "name"); + final Song song = new Song( + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, + Genre.from("댄스"), + killingParts + ); // when final String killingPart1VideoUrl = song.getPartVideoUrl(killingPart1); diff --git a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java new file mode 100644 index 000000000..dee658b31 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -0,0 +1,94 @@ +package shook.shook.song.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import shook.shook.song.exception.ArtistException; + +class SynonymTest { + + @DisplayName("가수 이름 동의어를 뜻하는 객체를 생성한다.") + @Test + void create_success() { + //given + //when + //then + assertDoesNotThrow(() -> new Synonym("동의어")); + } + + @DisplayName("가수 이름 동의어가 유효하지 않으면 예외를 던진다.") + @NullSource + @ParameterizedTest(name = "동의어가 \"{0}\" 일 때") + @ValueSource(strings = {"", " "}) + void create_fail_lessThanOne(final String synonym) { + //given + //when + //then + assertThatThrownBy(() -> new Synonym(synonym)) + .isInstanceOf(ArtistException.NullOrEmptySynonymException.class); + } + + @DisplayName("가수 이름 동의어의 길이가 255를 넘을 경우 예외를 던진다.") + @Test + void create_fail_lengthOver255() { + //given + final String synonym = ".".repeat(256); + + //when + //then + assertThatThrownBy(() -> new Synonym(synonym)) + .isInstanceOf(ArtistException.TooLongSynonymException.class); + } + + @DisplayName("입력값으로 시작하는 동의어라면 true, 아니면 false 를 반환한다. (대소문자, 공백 제거)") + @ParameterizedTest + @CsvSource(value = {"Hi:true", "H i:true", "HiYou:true", " Hi:true", + "Hello:false"}, delimiter = ':') + void startsWithIgnoringCaseAndWhiteSpace(final String value, final boolean isSame) { + // given + final String keyword = "hi"; + final Synonym synonym = new Synonym(value); + + // when + final boolean result = synonym.startsWithIgnoringCaseAndWhiteSpace(keyword); + + // then + assertThat(result).isEqualTo(isSame); + } + + @DisplayName("입력값으로 끝나는 동의어라면 true, 아니면 false 를 반환한다. (대소문자, 공백 제거)") + @ParameterizedTest + @CsvSource(value = {"HelloHi:true", "H i:true", " Hi :true", "Hello:false"}, delimiter = ':') + void endsWithIgnoringCaseAndWhiteSpace(final String value, final boolean isSame) { + // given + final String keyword = "hi"; + final Synonym synonym = new Synonym(value); + + // when + final boolean result = synonym.endsWithIgnoringCaseAndWhiteSpace(keyword); + + // then + assertThat(result).isEqualTo(isSame); + } + + @DisplayName("입력값이 비어있다면 false 가 반환된다.") + @Test + void ignoringCaseAndWhiteSpace_emptyValue() { + // given + final String keyword = " "; + final Synonym synonym = new Synonym("hi"); + + // when + final boolean result = synonym.endsWithIgnoringCaseAndWhiteSpace(keyword); + + // then + assertThat(result).isFalse(); + } +} diff --git a/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartTest.java b/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartTest.java index f8d784127..d9ce803d8 100644 --- a/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartTest.java +++ b/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import shook.shook.member.domain.Member; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; import shook.shook.song.domain.Song; @@ -140,10 +141,15 @@ void setSong_alreadyRegisteredToSong_fail() { final KillingPart dummyKillingPart1 = KillingPart.forSave(0, 10); final KillingPart dummyKillingPart2 = KillingPart.forSave(0, 5); final KillingPart dummyKillingPart3 = KillingPart.forSave(0, 15); - final Song song = new Song("title", "elevenVideo", "imageUrl", "singer", 10, - Genre.from("댄스"), - new KillingParts( - List.of(dummyKillingPart1, dummyKillingPart2, dummyKillingPart3)) + final Artist artist = new Artist("image", "name"); + final Song song = new Song( + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, + Genre.from("댄스"), + new KillingParts(List.of(dummyKillingPart1, dummyKillingPart2, dummyKillingPart3)) ); final KillingPart killingPart = KillingPart.forSave(0, 10); diff --git a/backend/src/test/java/shook/shook/song/domain/killingpart/repository/KillingPartRepositoryTest.java b/backend/src/test/java/shook/shook/song/domain/killingpart/repository/KillingPartRepositoryTest.java index f46284589..21a214fa1 100644 --- a/backend/src/test/java/shook/shook/song/domain/killingpart/repository/KillingPartRepositoryTest.java +++ b/backend/src/test/java/shook/shook/song/domain/killingpart/repository/KillingPartRepositoryTest.java @@ -9,10 +9,12 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +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; import shook.shook.support.UsingJpaTest; @@ -27,6 +29,9 @@ class KillingPartRepositoryTest extends UsingJpaTest { @Autowired private KillingPartRepository killingPartRepository; + @Autowired + private ArtistRepository artistRepository; + @Autowired private SongRepository songRepository; @@ -42,8 +47,18 @@ void setUp() { THIRD_KILLING_PART ) ); - SAVED_SONG = songRepository.save( - new Song("제목", "비디오ID는 11글자", "이미지URL", "가수", 30, Genre.from("댄스"), KILLING_PARTS)); + final Artist artist = new Artist("image", "name"); + final Song song = new Song( + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, + Genre.from("댄스"), + KILLING_PARTS + ); + artistRepository.save(song.getArtist()); + SAVED_SONG = songRepository.save(song); } @DisplayName("KillingPart 를 모두 저장한다.") @@ -55,13 +70,12 @@ void save() { KILLING_PARTS.getKillingParts()); //then - assertThat(savedKillingParts).hasSize(3); - assertThat(savedKillingParts).containsExactly( - FIRST_KILLING_PART, - SECOND_KILLING_PART, - THIRD_KILLING_PART - ); - assertThat(savedKillingParts).usingRecursiveComparison() + assertThat(savedKillingParts).hasSize(3) + .containsExactly( + FIRST_KILLING_PART, + SECOND_KILLING_PART, + THIRD_KILLING_PART + ).usingRecursiveComparison() .comparingOnlyFields("id") .isNotNull(); } diff --git a/backend/src/test/java/shook/shook/song/domain/repository/SongRepositoryTest.java b/backend/src/test/java/shook/shook/song/domain/repository/SongRepositoryTest.java index 270162f15..1fab977f2 100644 --- a/backend/src/test/java/shook/shook/song/domain/repository/SongRepositoryTest.java +++ b/backend/src/test/java/shook/shook/song/domain/repository/SongRepositoryTest.java @@ -1,6 +1,7 @@ package shook.shook.song.domain.repository; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -12,6 +13,7 @@ import org.springframework.data.domain.PageRequest; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; import shook.shook.song.domain.Song; @@ -36,14 +38,24 @@ class SongRepositoryTest extends UsingJpaTest { @Autowired private MemberRepository memberRepository; + @Autowired + private ArtistRepository artistRepository; + 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"); return new Song( - "제목", "비디오ID는 11글자", "이미지URL", "가수", 5, Genre.from("댄스"), - new KillingParts(List.of(firstKillingPart, secondKillingPart, thirdKillingPart))); + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, + Genre.from("댄스"), + new KillingParts(List.of(firstKillingPart, secondKillingPart, thirdKillingPart)) + ); } private Member createAndSaveMember(final String email, final String name) { @@ -58,19 +70,24 @@ void save() { final Song song = createNewSongWithKillingParts(); //when - final Song savedSong = songRepository.save(song); + final Song savedSong = saveSong(song); //then assertThat(song).isSameAs(savedSong); assertThat(savedSong.getId()).isNotNull(); } + private Song saveSong(final Song song) { + artistRepository.save(song.getArtist()); + return songRepository.save(song); + } + @DisplayName("Id로 Song 을 조회한다.") @Test void findById() { //given final Song song = createNewSongWithKillingParts(); - songRepository.save(song); + saveSong(song); killingPartRepository.saveAll(song.getKillingParts()); //when @@ -78,9 +95,8 @@ void findById() { final Optional findSong = songRepository.findById(song.getId()); //then - assertThat(findSong).isPresent(); - assertThat(findSong.get()).usingRecursiveComparison() - .isEqualTo(song); + assertThat(findSong).isPresent() + .get().isEqualTo(song); } @DisplayName("Song 을 저장할 때의 시간 정보로 createAt이 자동 생성된다.") @@ -91,7 +107,7 @@ void createdAt_prePersist() { //when final LocalDateTime prev = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); - final Song saved = songRepository.save(song); + final Song saved = saveSong(song); final LocalDateTime after = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); //then @@ -105,9 +121,9 @@ void findAllWithTotalLikeCount() { // given final Member firstMember = createAndSaveMember("first@naver.com", "first"); final Member secondMember = createAndSaveMember("second@naver.com", "second"); - final Song firstSong = songRepository.save(createNewSongWithKillingParts()); - final Song secondSong = songRepository.save(createNewSongWithKillingParts()); - final Song thirdSong = songRepository.save(createNewSongWithKillingParts()); + final Song firstSong = saveSong(createNewSongWithKillingParts()); + final Song secondSong = saveSong(createNewSongWithKillingParts()); + final Song thirdSong = saveSong(createNewSongWithKillingParts()); killingPartRepository.saveAll(firstSong.getKillingParts()); killingPartRepository.saveAll(secondSong.getKillingParts()); @@ -141,17 +157,17 @@ private void addLikeToKillingPart(final KillingPart killingPart, final Member me void findSongsWithLessLikeCountThanSongWithId() { // given final Member member = createAndSaveMember("first@naver.com", "first"); - final Song eleventhSong = songRepository.save(createNewSongWithKillingParts()); - final Song tenthSong = songRepository.save(createNewSongWithKillingParts()); - final Song ninthSong = songRepository.save(createNewSongWithKillingParts()); - final Song eighthSong = songRepository.save(createNewSongWithKillingParts()); - final Song seventhSong = songRepository.save(createNewSongWithKillingParts()); - final Song sixthSong = songRepository.save(createNewSongWithKillingParts()); - final Song fifthSong = songRepository.save(createNewSongWithKillingParts()); - final Song fourthSong = songRepository.save(createNewSongWithKillingParts()); - final Song thirdSong = songRepository.save(createNewSongWithKillingParts()); - final Song secondSong = songRepository.save(createNewSongWithKillingParts()); - final Song standardSong = songRepository.save(createNewSongWithKillingParts()); + final Song eleventhSong = saveSong(createNewSongWithKillingParts()); + final Song tenthSong = saveSong(createNewSongWithKillingParts()); + final Song ninthSong = saveSong(createNewSongWithKillingParts()); + final Song eighthSong = saveSong(createNewSongWithKillingParts()); + final Song seventhSong = saveSong(createNewSongWithKillingParts()); + final Song sixthSong = saveSong(createNewSongWithKillingParts()); + final Song fifthSong = saveSong(createNewSongWithKillingParts()); + final Song fourthSong = saveSong(createNewSongWithKillingParts()); + final Song thirdSong = saveSong(createNewSongWithKillingParts()); + final Song secondSong = saveSong(createNewSongWithKillingParts()); + final Song standardSong = saveSong(createNewSongWithKillingParts()); killingPartRepository.saveAll(standardSong.getKillingParts()); killingPartRepository.saveAll(secondSong.getKillingParts()); @@ -193,22 +209,36 @@ void findSongsWithLessLikeCountThanSongWithId() { // then assertThat(songs).usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo(List.of(secondSong, thirdSong, fourthSong, fifthSong, sixthSong, seventhSong, - eighthSong, ninthSong, tenthSong, eleventhSong) + .isEqualTo(List.of( + findSavedSong(secondSong), + findSavedSong(thirdSong), + findSavedSong(fourthSong), + findSavedSong(fifthSong), + findSavedSong(sixthSong), + findSavedSong(seventhSong), + findSavedSong(eighthSong), + findSavedSong(ninthSong), + findSavedSong(tenthSong), + findSavedSong(eleventhSong) + ) ); } + private Song findSavedSong(final Song song) { + return songRepository.findById(song.getId()).get(); + } + @DisplayName("주어진 id보다 좋아요가 적은 노래 10개를 조회한다. (데이터가 기준보다 적을 때)") @Test void findSongsWithLessLikeCountThanSongWithId_SmallData() { // given final Member firstMember = createAndSaveMember("first@naver.com", "first"); - final Song firstSong = songRepository.save(createNewSongWithKillingParts()); - final Song secondSong = songRepository.save(createNewSongWithKillingParts()); - final Song thirdSong = songRepository.save(createNewSongWithKillingParts()); - final Song standardSong = songRepository.save(createNewSongWithKillingParts()); - final Song fifthSong = songRepository.save(createNewSongWithKillingParts()); + final Song firstSong = saveSong(createNewSongWithKillingParts()); + final Song secondSong = saveSong(createNewSongWithKillingParts()); + final Song thirdSong = saveSong(createNewSongWithKillingParts()); + final Song standardSong = saveSong(createNewSongWithKillingParts()); + final Song fifthSong = saveSong(createNewSongWithKillingParts()); killingPartRepository.saveAll(firstSong.getKillingParts()); killingPartRepository.saveAll(secondSong.getKillingParts()); @@ -243,7 +273,7 @@ void findSongsWithLessLikeCountThanSongWithId_SmallData() { // then assertThat(songs).usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo(List.of(thirdSong)); + .isEqualTo(List.of(findSavedSong(thirdSong))); } @DisplayName("주어진 id보다 좋아요가 많은 노래 10개를 총 좋아요 오름차순, id 오름차순으로 조회한다. (데이터가 충분할 때)") @@ -251,17 +281,17 @@ void findSongsWithLessLikeCountThanSongWithId_SmallData() { void findSongsWithMoreLikeCountThanSongWithId() { // given final Member member = createAndSaveMember("first@naver.com", "first"); - final Song firstSong = songRepository.save(createNewSongWithKillingParts()); - final Song secondSong = songRepository.save(createNewSongWithKillingParts()); - final Song thirdSong = songRepository.save(createNewSongWithKillingParts()); - final Song fourthSong = songRepository.save(createNewSongWithKillingParts()); - final Song fifthSong = songRepository.save(createNewSongWithKillingParts()); - final Song standardSong = songRepository.save(createNewSongWithKillingParts()); - final Song seventhSong = songRepository.save(createNewSongWithKillingParts()); - final Song eighthSong = songRepository.save(createNewSongWithKillingParts()); - final Song ninthSong = songRepository.save(createNewSongWithKillingParts()); - final Song tenthSong = songRepository.save(createNewSongWithKillingParts()); - final Song eleventhSong = songRepository.save(createNewSongWithKillingParts()); + final Song firstSong = saveSong(createNewSongWithKillingParts()); + final Song secondSong = saveSong(createNewSongWithKillingParts()); + final Song thirdSong = saveSong(createNewSongWithKillingParts()); + final Song fourthSong = saveSong(createNewSongWithKillingParts()); + final Song fifthSong = saveSong(createNewSongWithKillingParts()); + final Song standardSong = saveSong(createNewSongWithKillingParts()); + final Song seventhSong = saveSong(createNewSongWithKillingParts()); + final Song eighthSong = saveSong(createNewSongWithKillingParts()); + final Song ninthSong = saveSong(createNewSongWithKillingParts()); + final Song tenthSong = saveSong(createNewSongWithKillingParts()); + final Song eleventhSong = saveSong(createNewSongWithKillingParts()); killingPartRepository.saveAll(firstSong.getKillingParts()); killingPartRepository.saveAll(secondSong.getKillingParts()); @@ -295,6 +325,7 @@ void findSongsWithMoreLikeCountThanSongWithId() { // when saveAndClearEntityManager(); + final List songs = songRepository.findSongsWithMoreLikeCountThanSongWithId( standardSong.getId(), PageRequest.of(0, 10) @@ -303,9 +334,18 @@ void findSongsWithMoreLikeCountThanSongWithId() { // then assertThat(songs).usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo( - List.of(seventhSong, eighthSong, ninthSong, tenthSong, eleventhSong, fourthSong, - fifthSong, firstSong, secondSong, thirdSong)); + .isEqualTo(List.of( + findSavedSong(seventhSong), + findSavedSong(eighthSong), + findSavedSong(ninthSong), + findSavedSong(tenthSong), + findSavedSong(eleventhSong), + findSavedSong(fourthSong), + findSavedSong(fifthSong), + findSavedSong(firstSong), + findSavedSong(secondSong), + findSavedSong(thirdSong) + )); } @DisplayName("주어진 id보다 좋아요가 많은 노래 10개를 총 좋아요 오름차순, id 오름차순으로 조회한다. (데이터가 기준보다 부족할 때)") @@ -313,12 +353,12 @@ void findSongsWithMoreLikeCountThanSongWithId() { void findSongsWithMoreLikeCountThanSongWithId_smallData() { // given final Member member = createAndSaveMember("first@naver.com", "first"); - final Song firstSong = songRepository.save(createNewSongWithKillingParts()); - final Song secondSong = songRepository.save(createNewSongWithKillingParts()); - final Song thirdSong = songRepository.save(createNewSongWithKillingParts()); - final Song fourthSong = songRepository.save(createNewSongWithKillingParts()); - final Song fifthSong = songRepository.save(createNewSongWithKillingParts()); - final Song standardSong = songRepository.save(createNewSongWithKillingParts()); + final Song firstSong = saveSong(createNewSongWithKillingParts()); + final Song secondSong = saveSong(createNewSongWithKillingParts()); + final Song thirdSong = saveSong(createNewSongWithKillingParts()); + final Song fourthSong = saveSong(createNewSongWithKillingParts()); + final Song fifthSong = saveSong(createNewSongWithKillingParts()); + final Song standardSong = saveSong(createNewSongWithKillingParts()); killingPartRepository.saveAll(firstSong.getKillingParts()); killingPartRepository.saveAll(secondSong.getKillingParts()); @@ -355,7 +395,47 @@ void findSongsWithMoreLikeCountThanSongWithId_smallData() { // then assertThat(songs).usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo( - List.of(fourthSong, fifthSong, firstSong, secondSong, thirdSong)); + .isEqualTo(List.of( + findSavedSong(fourthSong), + findSavedSong(fifthSong), + findSavedSong(firstSong), + findSavedSong(secondSong), + findSavedSong(thirdSong)) + ); + } + + @DisplayName("Artist 의 모든 노래를 좋아요 개수와 함께 조회한다.") + @Test + void findAllSongsWithTotalLikeCountByArtist() { + // given + final Member firstMember = createAndSaveMember("first@naver.com", "first"); + final Member secondMember = createAndSaveMember("second@naver.com", "second"); + final Song firstSong = saveSong(createNewSongWithKillingParts()); + final Song secondSong = saveSong(createNewSongWithKillingParts()); + final Song thirdSong = saveSong(createNewSongWithKillingParts()); + + killingPartRepository.saveAll(firstSong.getKillingParts()); + killingPartRepository.saveAll(secondSong.getKillingParts()); + killingPartRepository.saveAll(thirdSong.getKillingParts()); + + addLikeToKillingPart(firstSong.getKillingParts().get(0), firstMember); + addLikeToKillingPart(firstSong.getKillingParts().get(0), secondMember); + addLikeToKillingPart(firstSong.getKillingParts().get(1), firstMember); + addLikeToKillingPart(firstSong.getKillingParts().get(2), firstMember); + + saveAndClearEntityManager(); + + // when + final Song songToFind = songRepository.findById(firstSong.getId()).get(); + final Artist artistToFind = songToFind.getArtist(); + final List result = songRepository.findAllSongsWithTotalLikeCountByArtist( + artistToFind); + + // then + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).getSong()).isEqualTo(songToFind), + () -> assertThat(result.get(0).getTotalLikeCount()).isEqualTo(4) + ); } } diff --git a/backend/src/test/java/shook/shook/song/ui/AdminSongControllerTest.java b/backend/src/test/java/shook/shook/song/ui/AdminSongControllerTest.java index a79f5608d..15f18badd 100644 --- a/backend/src/test/java/shook/shook/song/ui/AdminSongControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/AdminSongControllerTest.java @@ -34,7 +34,13 @@ void setUp() { void register_success() { // given final SongWithKillingPartsRegisterRequest request = new SongWithKillingPartsRegisterRequest( - "title1", "elevenVideo", "imageUrl", "singer", 300, "댄스", + "title", + "elevenVideo", + "imageUrl", + "singer", + "image", + 300, + "댄스", List.of( new KillingPartRegisterRequest(10, 5), new KillingPartRegisterRequest(15, 10), @@ -51,29 +57,4 @@ void register_success() { .then().log().all() .statusCode(HttpStatus.CREATED.value()); } - - @DisplayName("노래와 킬링파트 등록시 이미 존재하는 노래일 경우 401 상태코드를 반환한다.") - @Test - void register_alreadyExist() { - // given - final SongWithKillingPartsRegisterRequest request = new SongWithKillingPartsRegisterRequest( - "title2", "elevenVideo", "imageUrl", "singer", 300, "댄스", - List.of( - new KillingPartRegisterRequest(10, 5), - new KillingPartRegisterRequest(15, 10), - new KillingPartRegisterRequest(0, 10) - ) - ); - - songService.register(request); - - // when, then - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(request) - .when().log().all() - .post("/songs") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); - } } diff --git a/backend/src/test/java/shook/shook/song/ui/ArtistControllerTest.java b/backend/src/test/java/shook/shook/song/ui/ArtistControllerTest.java new file mode 100644 index 000000000..c258b568a --- /dev/null +++ b/backend/src/test/java/shook/shook/song/ui/ArtistControllerTest.java @@ -0,0 +1,101 @@ +package shook.shook.song.ui; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.restassured.RestAssured; +import java.util.List; +import java.util.Map; +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.ArtistWithSongSearchResponse; +import shook.shook.song.application.dto.SongSearchResponse; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistSynonym; +import shook.shook.song.domain.InMemoryArtistSynonyms; +import shook.shook.song.domain.Synonym; +import shook.shook.song.domain.repository.ArtistRepository; + +@SuppressWarnings("NonAsciiCharacters") +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ArtistControllerTest { + + private Artist newJeans; + private Artist 가수; + private Artist 정국; + + @LocalServerPort + public int port; + + @BeforeEach + void setUp() { + RestAssured.port = port; + + newJeans = artistRepository.findById(1L).get(); + 가수 = artistRepository.findById(2L).get(); + 정국 = artistRepository.findById(3L).get(); + final ArtistSynonym synonym1 = new ArtistSynonym(newJeans, new Synonym("인기뉴진스")); + final ArtistSynonym synonym2 = new ArtistSynonym(가수, new Synonym("인기가수")); + final ArtistSynonym synonym3 = new ArtistSynonym(정국, new Synonym("방탄인기")); + + artistSynonyms.initialize( + Map.of( + synonym1, newJeans, + synonym2, 가수, + synonym3, 정국 + ) + ); + } + + @Autowired + private InMemoryArtistSynonyms artistSynonyms; + + @Autowired + private ArtistRepository artistRepository; + + @DisplayName("GET /singers/{singerId} 로 요청을 보내는 경우 상태코드 200, 해당 가수의 정보, 모든 노래 리스트를 반환한다.") + @Test + void searchSongsByArtist() { + // given + final Long singerId = newJeans.getId(); + + // when + final ArtistWithSongSearchResponse response = RestAssured.given().log().all() + .when().log().all() + .get("/singers/{singerId}", singerId) + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .body().as(ArtistWithSongSearchResponse.class); + + // then + assertThat(response.getId()).isEqualTo(newJeans.getId()); + assertThat(getSongIdsFromResponse(response)).containsExactly(3L, 1L); + } + + private List getSongIdsFromResponse(final ArtistWithSongSearchResponse response) { + return response.getSongs() + .stream() + .map(SongSearchResponse::getId) + .toList(); + } + + @DisplayName("PUT /singers/synonyms 로 요청을 보내는 경우 DB에 저장된 가수, 동의어와 가수를 동기화한다.") + @Test + void updateArtistSynonym() { + // given + // when, then + RestAssured.given().log().all() + .when().log().all() + .put("/singers/synonyms") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } +} diff --git a/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java b/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java index fecd52070..653d29958 100644 --- a/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java @@ -14,6 +14,7 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; import shook.shook.song.application.InMemorySongsScheduler; import shook.shook.song.application.killingpart.KillingPartLikeService; import shook.shook.song.application.killingpart.dto.HighLikedSongResponse; @@ -21,6 +22,7 @@ @Sql("classpath:/killingpart/initialize_killing_part_song.sql") @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@Transactional class HighLikedSongControllerTest { private static final long FIRST_SONG_ID = 1L; diff --git a/backend/src/test/java/shook/shook/song/ui/MyPageControllerTest.java b/backend/src/test/java/shook/shook/song/ui/MyPageControllerTest.java index 114dcfe0b..9016cf812 100644 --- a/backend/src/test/java/shook/shook/song/ui/MyPageControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/MyPageControllerTest.java @@ -118,13 +118,6 @@ void likedKillingPartExistWithOneDeletedLikeExist() { //when //then - - final List expected = List.of( - LikedKillingPartResponse.of(thirdSong, thirdSongKillingPart.get(0)), - LikedKillingPartResponse.of(secondSong, secondSongKillingPart.get(0)), - LikedKillingPartResponse.of(firstSong, firstSongKillingPart.get(0)) - ); - final List response = RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken) .contentType(ContentType.JSON) @@ -134,7 +127,15 @@ void likedKillingPartExistWithOneDeletedLikeExist() { .statusCode(HttpStatus.OK.value()) .extract().body().jsonPath().getList(".", LikedKillingPartResponse.class); - assertThat(response).usingRecursiveComparison().isEqualTo(expected); + assertThat(response.get(0)) + .hasFieldOrPropertyWithValue("songId", thirdSong.getId()) + .hasFieldOrPropertyWithValue("partId", thirdSongKillingPart.get(0).getId()); + assertThat(response.get(1)) + .hasFieldOrPropertyWithValue("songId", secondSong.getId()) + .hasFieldOrPropertyWithValue("partId", secondSongKillingPart.get(0).getId()); + assertThat(response.get(2)) + .hasFieldOrPropertyWithValue("songId", firstSong.getId()) + .hasFieldOrPropertyWithValue("partId", firstSongKillingPart.get(0).getId()); } @DisplayName("좋아요한 킬링파트가 없을 때") diff --git a/backend/src/test/java/shook/shook/song/ui/SearchControllerTest.java b/backend/src/test/java/shook/shook/song/ui/SearchControllerTest.java new file mode 100644 index 000000000..97af62d1f --- /dev/null +++ b/backend/src/test/java/shook/shook/song/ui/SearchControllerTest.java @@ -0,0 +1,129 @@ +package shook.shook.song.ui; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import io.restassured.RestAssured; +import java.util.List; +import java.util.Map; +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.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.application.dto.SongSearchResponse; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistSynonym; +import shook.shook.song.domain.InMemoryArtistSynonyms; +import shook.shook.song.domain.Synonym; +import shook.shook.song.domain.repository.ArtistRepository; + +@SuppressWarnings("NonAsciiCharacters") +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class SearchControllerTest { + + private Artist newJeans; + private Artist 가수; + private Artist 정국; + + @LocalServerPort + public int port; + + @BeforeEach + void setUp() { + RestAssured.port = port; + + newJeans = artistRepository.findById(1L).get(); + 가수 = artistRepository.findById(2L).get(); + 정국 = artistRepository.findById(3L).get(); + final ArtistSynonym synonym1 = new ArtistSynonym(newJeans, new Synonym("인기뉴진스")); + final ArtistSynonym synonym2 = new ArtistSynonym(가수, new Synonym("인기가수")); + final ArtistSynonym synonym3 = new ArtistSynonym(정국, new Synonym("방탄인기")); + + artistSynonyms.initialize( + Map.of( + synonym1, newJeans, + synonym2, 가수, + synonym3, 정국 + ) + ); + } + + @Autowired + private InMemoryArtistSynonyms artistSynonyms; + + @Autowired + private ArtistRepository artistRepository; + + @DisplayName("type=singer,song keyword=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하거나 끝나는 가수, 가수의 TOP3 노래 리스트를 반환한다.") + @Test + void searchArtistWithSongByKeyword() { + // given + final String searchType = "singer,song"; + final String keyword = "인기"; + + // when + final List response = RestAssured.given().log().all() + .params(Map.of("keyword", keyword, "type", searchType)) + .when().log().all() + .get("/search") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .body().jsonPath().getList(".", ArtistWithSongSearchResponse.class); + + // then + assertThat(response).hasSize(3); + final ArtistWithSongSearchResponse firstResponse = response.get(0); + assertThat(firstResponse.getId()).isEqualTo(newJeans.getId()); + assertThat(getSongIdsFromResponse(firstResponse)).containsExactly(3L, 1L); + + final ArtistWithSongSearchResponse secondResponse = response.get(1); + assertThat(secondResponse.getId()).isEqualTo(가수.getId()); + assertThat(getSongIdsFromResponse(secondResponse)).containsExactly(2L); + + final ArtistWithSongSearchResponse thirdResponse = response.get(2); + assertThat(thirdResponse.getId()).isEqualTo(정국.getId()); + assertThat(getSongIdsFromResponse(thirdResponse)).containsExactly(4L); + } + + private List getSongIdsFromResponse(final ArtistWithSongSearchResponse response) { + return response.getSongs() + .stream() + .map(SongSearchResponse::getId) + .toList(); + } + + @DisplayName("type=singer keyword=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하는 가수 목록을 반환한다.") + @Test + void searchArtistByKeyword() { + // given + final String searchType = "singer"; + final String keyword = "인기"; + + // when + final List response = RestAssured.given().log().all() + .params(Map.of("keyword", keyword, "type", searchType)) + .when().log().all() + .get("/search") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .body().jsonPath().getList(".", ArtistResponse.class); + + // then + // 가나다 순 정렬, 가수 -> 뉴진스 + assertAll( + () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0)).hasFieldOrPropertyWithValue("id", newJeans.getId()), + () -> assertThat(response.get(1)).hasFieldOrPropertyWithValue("id", 가수.getId()) + ); + } +} diff --git a/backend/src/test/java/shook/shook/song/ui/SongSwipeControllerTest.java b/backend/src/test/java/shook/shook/song/ui/SongSwipeControllerTest.java index 5a6a165a2..494b106b7 100644 --- a/backend/src/test/java/shook/shook/song/ui/SongSwipeControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/SongSwipeControllerTest.java @@ -25,6 +25,7 @@ import shook.shook.song.application.killingpart.dto.HighLikedSongResponse; import shook.shook.song.application.killingpart.dto.KillingPartLikeRequest; +@SuppressWarnings("NonAsciiCharacters") @Sql("classpath:/killingpart/initialize_killing_part_song.sql") @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class SongSwipeControllerTest { diff --git a/backend/src/test/java/shook/shook/voting_song/application/VotingSongPartServiceTest.java b/backend/src/test/java/shook/shook/voting_song/application/VotingSongPartServiceTest.java index c223cb647..ee3114251 100644 --- a/backend/src/test/java/shook/shook/voting_song/application/VotingSongPartServiceTest.java +++ b/backend/src/test/java/shook/shook/voting_song/application/VotingSongPartServiceTest.java @@ -14,6 +14,8 @@ import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.application.dto.VotingSongPartRegisterRequest; import shook.shook.voting_song.domain.Vote; @@ -42,6 +44,9 @@ class VotingSongPartServiceTest extends UsingJpaTest { @Autowired private VoteRepository voteRepository; + @Autowired + private ArtistRepository artistRepository; + private VotingSongPartService votingSongPartService; @BeforeEach @@ -54,7 +59,15 @@ void setUp() { ); FIRST_MEMBER = memberRepository.save(new Member("a@a.com", "nickname")); SECOND_MEMBER = memberRepository.save(new Member("b@b.com", "nickname")); - SAVED_SONG = votingSongRepository.save(new VotingSong("노래제목", "비디오ID는 11글자", "이미지URL", "가수", 180)); + final Artist artist = new Artist("profile", "가수"); + artistRepository.save(artist); + SAVED_SONG = votingSongRepository.save(new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180) + ); } void addPart(final VotingSong votingSong, final VotingSongPart votingSongPart) { @@ -102,8 +115,7 @@ void registered_membersSamePartExist() { //when final MemberInfo anotherMemberInfo = new MemberInfo(FIRST_MEMBER.getId(), Authority.MEMBER); - votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, SAVED_SONG.getId(), - request); + votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, SAVED_SONG.getId(), request); saveAndClearEntityManager(); //then @@ -127,8 +139,7 @@ void registered() { //when final MemberInfo anotherMemberInfo = new MemberInfo(SECOND_MEMBER.getId(), Authority.MEMBER); - votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, SAVED_SONG.getId(), - request); + votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, SAVED_SONG.getId(), request); saveAndClearEntityManager(); //then diff --git a/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java b/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java index 10609cf93..d8d48fc46 100644 --- a/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java +++ b/backend/src/test/java/shook/shook/voting_song/application/VotingSongServiceTest.java @@ -11,7 +11,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import shook.shook.song.domain.Artist; import shook.shook.song.domain.SongTitle; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.application.dto.VotingSongRegisterRequest; import shook.shook.voting_song.application.dto.VotingSongResponse; @@ -22,22 +24,47 @@ class VotingSongServiceTest extends UsingJpaTest { + private static final String VIDEO_ID = "비디오ID는 11글자"; + @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + private VotingSongService votingSongService; @BeforeEach void setUp() { - votingSongService = new VotingSongService(votingSongRepository); + votingSongService = new VotingSongService(votingSongRepository, artistRepository); + } + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist("profile", "가수"); + final VotingSong votingSong = new VotingSong( + votingSongTitle, + VIDEO_ID, + "이미지URL", + artist, + 180 + ); + + artistRepository.save(votingSong.getArtist()); + return votingSongRepository.save(votingSong); } @DisplayName("파트 수집 중인 노래를 등록한다.") @Test void register() { //given - final VotingSongRegisterRequest request = - new VotingSongRegisterRequest("새로운노래제목", "비디오ID는 11글자", "이미지URL", "가수", 180); + final VotingSongRegisterRequest request = new VotingSongRegisterRequest( + "새로운노래제목", + "비디오ID는 11글자", + "이미지URL", + "가수", + "프로필URL", + 180 + ); //when votingSongService.register(request); @@ -52,7 +79,7 @@ void register() { () -> assertThat(savedSong.getCreatedAt()).isNotNull(), () -> assertThat(savedSong.getTitle()).isEqualTo("새로운노래제목"), () -> assertThat(savedSong.getVideoId()).isEqualTo("비디오ID는 11글자"), - () -> assertThat(savedSong.getSinger()).isEqualTo("가수"), + () -> assertThat(savedSong.getArtistName()).isEqualTo("가수"), () -> assertThat(savedSong.getLength()).isEqualTo(180) ); } @@ -65,21 +92,12 @@ class findAll { @Test void findAllVotingSongs() { // given - final VotingSong firstSong = - votingSongRepository.save( - new VotingSong("노래1", "비디오ID는 11글자", "이미지URL", "가수", 180)); - final VotingSong secondSong = - votingSongRepository.save( - new VotingSong("노래2", "비디오ID는 11글자", "이미지URL", "가수", 180)); - final VotingSong thirdSong = - votingSongRepository.save( - new VotingSong("노래3", "비디오ID는 11글자", "이미지URL", "가수", 180)); - final VotingSong fourthSong = - votingSongRepository.save( - new VotingSong("노래4", "비디오ID는 11글자", "이미지URL", "가수", 180)); - final VotingSong fifthSong = - votingSongRepository.save( - new VotingSong("노래5", "비디오ID는 11글자", "이미지URL", "가수", 180)); + final Artist artist = new Artist("profile", "가수"); + final VotingSong firstSong = saveVotingSongWithTitle("노래1"); + final VotingSong secondSong = saveVotingSongWithTitle("노래2"); + final VotingSong thirdSong = saveVotingSongWithTitle("노래3"); + final VotingSong fourthSong = saveVotingSongWithTitle("노래4"); + final VotingSong fifthSong = saveVotingSongWithTitle("노래5"); final List expected = Stream.of(firstSong, secondSong, thirdSong, fourthSong, fifthSong) @@ -114,24 +132,12 @@ class findByPartForSwipe { @Test void success() { // given - final VotingSong firstSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong secondSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fourthSong = votingSongRepository.save( - new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fifthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong sixthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); + final VotingSong firstSong = saveVotingSongWithTitle("제목1"); + final VotingSong secondSong = saveVotingSongWithTitle("제목2"); + final VotingSong standardSong = saveVotingSongWithTitle("제목3"); + final VotingSong fourthSong = saveVotingSongWithTitle("제목4"); + final VotingSong fifthSong = saveVotingSongWithTitle("제목5"); + final VotingSong sixthSong = saveVotingSongWithTitle("제목5"); // when final VotingSongSwipeResponse swipeResponse = diff --git a/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartTest.java b/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartTest.java index fbd046553..cd4dd7934 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartTest.java @@ -10,12 +10,20 @@ import shook.shook.member.domain.Member; import shook.shook.part.domain.PartLength; import shook.shook.part.exception.PartException; +import shook.shook.song.domain.Artist; import shook.shook.voting_song.exception.VoteException; class VotingSongPartTest { private static Member MEMBER = new Member("a@a.com", "nickname"); - private final VotingSong votingSong = new VotingSong("제목", "비디오ID는 11글자", "이미지URL", "가수", 30); + private final Artist artist = new Artist("profile", "가수"); + private final VotingSong votingSong = new VotingSong( + "제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 30 + ); @DisplayName("Id가 같은 파트는 동등성 비교에 참을 반환한다.") @Test diff --git a/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartsTest.java b/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartsTest.java index 77dbd81eb..2af58c499 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartsTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/VotingSongPartsTest.java @@ -7,6 +7,7 @@ import shook.shook.member.domain.Member; import shook.shook.part.domain.PartLength; import shook.shook.part.exception.PartException; +import shook.shook.song.domain.Artist; class VotingSongPartsTest { @@ -16,7 +17,14 @@ class VotingSongPartsTest { @Test void create_fail_duplicatePartExist() { //given - final VotingSong votingSong = new VotingSong("제목", "비디오ID는 11글자", "이미지URL", "가수", 30); + final Artist artist = new Artist("profile", "가수"); + final VotingSong votingSong = new VotingSong( + "제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 30 + ); final VotingSongPart firstPart = VotingSongPart.saved(1L, 5, new PartLength(5), votingSong); final VotingSongPart secondPart = VotingSongPart.forSave(5, new PartLength(5), votingSong); diff --git a/backend/src/test/java/shook/shook/voting_song/domain/VotingSongTest.java b/backend/src/test/java/shook/shook/voting_song/domain/VotingSongTest.java index aa4930a06..1bb2b64b4 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/VotingSongTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/VotingSongTest.java @@ -6,19 +6,29 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Artist; import shook.shook.voting_song.exception.VotingSongPartException; class VotingSongTest { + private final Artist artist = new Artist("profile", "가수"); + @DisplayName("파트 수집 중인 노래에 파트를 등록한다. ( 노래에 해당하는 파트일 때 )") @Test void addPart_valid() { //given - final VotingSong votingSong = new VotingSong("노래제목", "비디오ID는 11글자", "이미지URL", "가수", 180); + final VotingSong votingSong = new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180 + ); final VotingSongPart votingSongPart = VotingSongPart.forSave(1, new PartLength(10), votingSong); //when votingSong.addPart(votingSongPart); + final String artistName = votingSong.getArtistName(); //then assertThat(votingSong.getParts()).hasSize(1); @@ -28,8 +38,20 @@ void addPart_valid() { @Test void addPart_invalid() { //given - final VotingSong firstSong = new VotingSong("노래제목", "비디오ID는 11글자", "이미지URL", "가수", 180); - final VotingSong secondSong = new VotingSong("노래제목", "비디오ID는 11글자", "이미지URL", "가수", 180); + final VotingSong firstSong = new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180 + ); + final VotingSong secondSong = new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180 + ); final VotingSongPart partInSecondSong = VotingSongPart.forSave(1, new PartLength(10), secondSong); //when diff --git a/backend/src/test/java/shook/shook/voting_song/domain/repository/VoteRepositoryTest.java b/backend/src/test/java/shook/shook/voting_song/domain/repository/VoteRepositoryTest.java index 2cec58111..5de4f70fe 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/repository/VoteRepositoryTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/repository/VoteRepositoryTest.java @@ -9,6 +9,8 @@ import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.Vote; import shook.shook.voting_song.domain.VotingSong; @@ -29,19 +31,34 @@ class VoteRepositoryTest extends UsingJpaTest { @Autowired private VotingSongPartRepository votingSongPartRepository; + @Autowired + private ArtistRepository artistRepository; + @DisplayName("투표중인 노래의 파트에 멤버의 투표가 존재하는지 반환한다.") @Test void existsByMemberAndVotingSongPart() { //given final Member member = memberRepository.findById(1L).get(); + final Artist artist = new Artist("profile", "가수"); + artistRepository.save(artist); final VotingSong votingSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 20)); + new VotingSong( + "제목1", + "비디오ID는 11글자", + "이미지URL", + artist, + 20) + ); final VotingSongPart votingSongPart = votingSongPartRepository.save( - VotingSongPart.forSave(1, new PartLength(5), votingSong)); + VotingSongPart.forSave(1, new PartLength(5), votingSong) + ); voteRepository.save(Vote.forSave(member, votingSongPart)); //when - final boolean isVoteExist = voteRepository.existsByMemberAndVotingSongPart(member, votingSongPart); + final boolean isVoteExist = voteRepository.existsByMemberAndVotingSongPart( + member, + votingSongPart + ); //then assertThat(isVoteExist).isTrue(); diff --git a/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongPartRepositoryTest.java b/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongPartRepositoryTest.java index b32148f85..69f421f49 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongPartRepositoryTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongPartRepositoryTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; import shook.shook.voting_song.domain.VotingSongPart; @@ -23,12 +25,24 @@ class VotingSongPartRepositoryTest extends UsingJpaTest { @Autowired private VotingSongRepository votingSongRepository; + + @Autowired + private ArtistRepository artistRepository; + private static VotingSong SAVED_SONG; @BeforeEach void setUp() { + final Artist artist = new Artist("profile", "가수"); + artistRepository.save(artist); SAVED_SONG = votingSongRepository.save( - new VotingSong("제목", "비디오ID는 11글자", "이미지URL", "가수", 30)); + new VotingSong( + "제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 30) + ); } @DisplayName("VotingSongPart 를 저장한다.") diff --git a/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java b/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java index b057b7046..d46f67107 100644 --- a/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java +++ b/backend/src/test/java/shook/shook/voting_song/domain/repository/VotingSongRepositoryTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -15,6 +17,23 @@ class VotingSongRepositoryTest extends UsingJpaTest { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist("profile", "가수"); + final VotingSong votingSong = new VotingSong( + votingSongTitle, + "12345678901", + "이미지URL", + artist, + 180 + ); + + artistRepository.save(artist); + return votingSongRepository.save(votingSong); + } + @DisplayName("특정 파트 수집 중인 노래 id 를 기준으로 id가 작은 노래를 조회한다.") @Nested class findSongsLessThanSongId { @@ -23,39 +42,17 @@ class findSongsLessThanSongId { @Test void enough() { // given - final VotingSong firstSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong secondSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong thirdSong = votingSongRepository.save( - new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fourthSong = votingSongRepository.save( - new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fifthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong seventhSong = votingSongRepository.save( - new VotingSong("제목7", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong eighthSong = votingSongRepository.save( - new VotingSong("제목8", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong ninthSong = votingSongRepository.save( - new VotingSong("제목9", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong tenthSong = votingSongRepository.save( - new VotingSong("제목10", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong eleventhSong = votingSongRepository.save( - new VotingSong("제목11", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); + final VotingSong firstSong = saveVotingSongWithTitle("제목1"); + final VotingSong secondSong = saveVotingSongWithTitle("제목2"); + final VotingSong thirdSong = saveVotingSongWithTitle("제목3"); + final VotingSong fourthSong = saveVotingSongWithTitle("제목4"); + final VotingSong fifthSong = saveVotingSongWithTitle("제목5"); + final VotingSong standardSong = saveVotingSongWithTitle("제목5"); + final VotingSong seventhSong = saveVotingSongWithTitle("제목7"); + final VotingSong eighthSong = saveVotingSongWithTitle("제목8"); + final VotingSong ninthSong = saveVotingSongWithTitle("제목9"); + final VotingSong tenthSong = saveVotingSongWithTitle("제목10"); + final VotingSong eleventhSong = saveVotingSongWithTitle("제목11"); // when final List beforeVotingSongs = votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( @@ -84,33 +81,15 @@ void enough() { @Test void prevSongNotEnough() { // given - final VotingSong firstSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong secondSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fourthSong = votingSongRepository.save( - new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fifthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong sixthSong = votingSongRepository.save( - new VotingSong("제목6", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong seventhSong = votingSongRepository.save( - new VotingSong("제목7", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong eighthSong = votingSongRepository.save( - new VotingSong("제목8", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong ninthSong = votingSongRepository.save( - new VotingSong("제목9", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); + final VotingSong firstSong = saveVotingSongWithTitle("제목1"); + final VotingSong secondSong = saveVotingSongWithTitle("제목2"); + final VotingSong standardSong = saveVotingSongWithTitle("제목3"); + final VotingSong fourthSong = saveVotingSongWithTitle("제목4"); + final VotingSong fifthSong = saveVotingSongWithTitle("제목5"); + final VotingSong sixthSong = saveVotingSongWithTitle("제목6"); + final VotingSong seventhSong = saveVotingSongWithTitle("제목7"); + final VotingSong eighthSong = saveVotingSongWithTitle("제목8"); + final VotingSong ninthSong = saveVotingSongWithTitle("제목9"); // when final List beforeVotingSongs = votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( @@ -137,33 +116,15 @@ void prevSongNotEnough() { @Test void nextSongNotEnough() { // given - final VotingSong firstSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong secondSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong thirdSong = votingSongRepository.save( - new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fourthSong = votingSongRepository.save( - new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fifthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong sixthSong = votingSongRepository.save( - new VotingSong("제목6", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목7", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong eighthSong = votingSongRepository.save( - new VotingSong("제목8", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong ninthSong = votingSongRepository.save( - new VotingSong("제목9", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); + final VotingSong firstSong = saveVotingSongWithTitle("제목1"); + final VotingSong secondSong = saveVotingSongWithTitle("제목2"); + final VotingSong thirdSong = saveVotingSongWithTitle("제목3"); + final VotingSong fourthSong = saveVotingSongWithTitle("제목4"); + final VotingSong fifthSong = saveVotingSongWithTitle("제목5"); + final VotingSong sixthSong = saveVotingSongWithTitle("제목6"); + final VotingSong standardSong = saveVotingSongWithTitle("제목7"); + final VotingSong eighthSong = saveVotingSongWithTitle("제목8"); + final VotingSong ninthSong = saveVotingSongWithTitle("제목9"); // when final List beforeVotingSongs = votingSongRepository.findByIdGreaterThanEqualAndIdLessThanEqual( @@ -190,24 +151,12 @@ void nextSongNotEnough() { @Test void bothNotEnough() { // given - final VotingSong firstSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong secondSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong thirdSong = votingSongRepository.save( - new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목4", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong fifthSong = votingSongRepository.save( - new VotingSong("제목5", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); - final VotingSong sixthSong = votingSongRepository.save( - new VotingSong("제목6", "비디오ID는 11글자", "이미지URL", "가수", 30) - ); + final VotingSong firstSong = saveVotingSongWithTitle("제목1"); + final VotingSong secondSong = saveVotingSongWithTitle("제목2"); + final VotingSong thirdSong = saveVotingSongWithTitle("제목3"); + final VotingSong standardSong = saveVotingSongWithTitle("제목4"); + final VotingSong fifthSong = saveVotingSongWithTitle("제목5"); + final VotingSong sixthSong = saveVotingSongWithTitle("제목6"); // when final List beforeVotingSongs = diff --git a/backend/src/test/java/shook/shook/voting_song/ui/VotingSongControllerTest.java b/backend/src/test/java/shook/shook/voting_song/ui/VotingSongControllerTest.java index 40d9f4b40..49b05492d 100644 --- a/backend/src/test/java/shook/shook/voting_song/ui/VotingSongControllerTest.java +++ b/backend/src/test/java/shook/shook/voting_song/ui/VotingSongControllerTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.AcceptanceTest; import shook.shook.voting_song.application.dto.VotingSongResponse; import shook.shook.voting_song.application.dto.VotingSongSwipeResponse; @@ -21,14 +23,29 @@ class VotingSongControllerTest extends AcceptanceTest { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist("profile", "가수"); + final VotingSong votingSong = new VotingSong( + votingSongTitle, + "12345678901", + "이미지URL", + artist, + 180 + ); + artistRepository.save(artist); + + return votingSongRepository.save(votingSong); + } + @DisplayName("노래 정보를 조회시 제목, 가수, 길이, URL, 킬링파트를 담은 응답을 반환한다.") @Test void showSongById() { //given - final VotingSong song1 = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 20)); - final VotingSong song2 = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 20)); + final VotingSong song1 = saveVotingSongWithTitle("제목1"); + final VotingSong song2 = saveVotingSongWithTitle("제목2"); final List expected = Stream.of(song1, song2) .map(VotingSongResponse::from) @@ -55,12 +72,9 @@ void showSongById() { @Test void findById() { // given - final VotingSong prevSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 20)); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 20)); - final VotingSong nextSong = votingSongRepository.save( - new VotingSong("제목3", "비디오ID는 11글자", "이미지URL", "가수", 20)); + final VotingSong prevSong = saveVotingSongWithTitle("제목1"); + final VotingSong standardSong = saveVotingSongWithTitle("제목2"); + final VotingSong nextSong = saveVotingSongWithTitle("제목3"); // when final VotingSongSwipeResponse response = RestAssured.given().log().all() @@ -93,10 +107,8 @@ void findById() { @Test void findByIdEmptyAfterSong() { // given - final VotingSong prevSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 20)); - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 20)); + final VotingSong prevSong = saveVotingSongWithTitle("제목1"); + final VotingSong standardSong = saveVotingSongWithTitle("제목2"); // when final VotingSongSwipeResponse response = RestAssured.given().log().all() @@ -125,10 +137,8 @@ void findByIdEmptyAfterSong() { @Test void findByIdEmptyBeforeSong() { // given - final VotingSong standardSong = votingSongRepository.save( - new VotingSong("제목1", "비디오ID는 11글자", "이미지URL", "가수", 20)); - final VotingSong nextSong = votingSongRepository.save( - new VotingSong("제목2", "비디오ID는 11글자", "이미지URL", "가수", 20)); + final VotingSong standardSong = saveVotingSongWithTitle("제목1"); + final VotingSong nextSong = saveVotingSongWithTitle("제목2"); // when final VotingSongSwipeResponse response = RestAssured.given().log().all() diff --git a/backend/src/test/java/shook/shook/voting_song/ui/VotingSongPartControllerTest.java b/backend/src/test/java/shook/shook/voting_song/ui/VotingSongPartControllerTest.java index 77f1a8fb1..0817e5184 100644 --- a/backend/src/test/java/shook/shook/voting_song/ui/VotingSongPartControllerTest.java +++ b/backend/src/test/java/shook/shook/voting_song/ui/VotingSongPartControllerTest.java @@ -17,6 +17,8 @@ import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.voting_song.application.VotingSongPartService; import shook.shook.voting_song.application.dto.VotingSongPartRegisterRequest; import shook.shook.voting_song.domain.VotingSong; @@ -43,6 +45,9 @@ void setUp() { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + @Autowired private VotingSongPartService votingSongPartService; @@ -89,7 +94,15 @@ private String getToken(final Long memberId, final String nickname) { } private VotingSong getSavedSong() { - return votingSongRepository.save(new VotingSong("title", "12345678901", "albumCover", "singer", 100)); + final Artist artist = new Artist("profile", "가수"); + artistRepository.save(artist); + return votingSongRepository.save(new VotingSong( + "title", + "12345678901", + "albumCover", + artist, + 100) + ); } private Member getMember() { diff --git a/backend/src/test/resources/killingpart/initialize_killing_part_song.sql b/backend/src/test/resources/killingpart/initialize_killing_part_song.sql index 88da34a14..956e22364 100644 --- a/backend/src/test/resources/killingpart/initialize_killing_part_song.sql +++ b/backend/src/test/resources/killingpart/initialize_killing_part_song.sql @@ -1,6 +1,7 @@ drop table song; drop table killing_part; drop table member; +drop table artist; drop table killing_part_like; drop table killing_part_comment; drop table member_part; @@ -9,7 +10,7 @@ create table if not exists song ( id bigint auto_increment, title varchar(100) not null, - singer varchar(50) not null, + artist_id bigint not null, length integer not null, video_id varchar(20) not null, album_cover_url text not null, @@ -73,17 +74,30 @@ create table if not exists member_part primary key (id) ); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at, genre) -VALUES ('Super Shy', 'NewJeans', 200, 'ArmDp-zijuc', +create table if not exists artist +( + id bigint generated by default as identity, + name varchar(50) not null, + profile_image_url text not null, + created_at timestamp(6) not null, + primary key (id) +); + +INSERT INTO artist (name, profile_image_url, created_at) values ('NewJeans', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); +INSERT INTO artist (name, profile_image_url, created_at) values ('가수', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); +INSERT INTO artist (name, profile_image_url, created_at) values ('정국', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now()); + +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, created_at, genre) +VALUES ('Super Shy', 1, 200, 'ArmDp-zijuc', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now(), 'DANCE'); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at, genre) -VALUES ('노래', '가수', 263, 'sjeifksl', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, created_at, genre) +VALUES ('노래', 2, 263, 'sjeifksl', 'http://i.maniadb.com/images/album/29382/028492.jpg', now(), 'HIPHOP'); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at, genre) -VALUES ('Not Shy', 'NewJeans', 200, 'ArmDp-zijuc', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, created_at, genre) +VALUES ('Not Shy', 1, 200, 'ArmDp-zijuc', 'http://i.maniadb.com/images/album/999/999126_1_f.jpg', now(), 'DANCE'); -INSERT INTO song (title, singer, length, video_id, album_cover_url, created_at, genre) -VALUES ('Seven (feat. Latto) - Clean Ver.', '정국', 186, 'UUSbUBYqU_8', +INSERT INTO song (title, artist_id, length, video_id, album_cover_url, created_at, genre) +VALUES ('Seven (feat. Latto) - Clean Ver.', 3, 186, 'UUSbUBYqU_8', 'http://i.maniadb.com/images/album/1000/000246_1_f.jpg', now(), 'DANCE'); INSERT INTO killing_part (start_second, length, song_id, like_count, created_at) diff --git a/backend/src/test/resources/schema-test.sql b/backend/src/test/resources/schema-test.sql index 6fc289aee..d3598dd75 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -7,12 +7,14 @@ drop table if exists voting_song; drop table if exists vote; drop table if exists member; drop table if exists member_part; +drop table if exists artist; +drop table if exists artist_synonym; create table if not exists song ( id bigint auto_increment, title varchar(100) not null, - singer varchar(50) not null, + artist_id bigint not null, length integer not null, video_id varchar(20) not null, album_cover_url text not null, @@ -60,9 +62,9 @@ create table if not exists voting_song ( id bigint auto_increment, title varchar(100) not null, - singer varchar(50) not null, length integer not null, video_id varchar(20) not null, + artist_id bigint not null, album_cover_url text not null, created_at timestamp(6) not null, primary key (id) @@ -104,3 +106,20 @@ create table if not exists member_part created_at timestamp(6) not null, primary key (id) ); + +create table if not exists artist +( + id bigint auto_increment, + name varchar(50) not null, + profile_image_url text not null, + created_at timestamp(6) not null, + primary key (id) +); + +create table if not exists artist_synonym +( + id bigint auto_increment, + artist_id bigint not null, + synonym varchar(255) not null, + primary key (id) +);