From 8672aa69a28a283518965311b563b2d4441efb37 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Mon, 9 Oct 2023 23:00:16 +0900 Subject: [PATCH 01/21] =?UTF-8?q?feat:=20Artist=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20Song=20singer=20->?= =?UTF-8?q?=20artist=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/globalexception/ErrorCode.java | 3 + .../GlobalExceptionHandler.java | 4 +- .../song/application/SongDataExcelReader.java | 7 +- .../shook/song/application/SongService.java | 29 ++-- .../dto/LikedKillingPartResponse.java | 2 +- .../song/application/dto/SongResponse.java | 2 +- .../SongWithKillingPartsRegisterRequest.java | 20 ++- .../dto/HighLikedSongResponse.java | 2 +- .../java/shook/shook/song/domain/Artist.java | 70 ++++++++ .../shook/shook/song/domain/ArtistName.java | 39 +++++ .../shook/song/domain/ProfileImageUrl.java | 39 +++++ .../java/shook/shook/song/domain/Singer.java | 7 +- .../java/shook/shook/song/domain/Song.java | 25 +-- .../domain/repository/ArtistRepository.java | 8 + .../shook/song/exception/ArtistException.java | 63 +++++++ .../shook/song/exception/SongException.java | 22 --- .../application/VotingSongService.java | 6 +- .../dto/VotingSongRegisterRequest.java | 16 +- .../application/dto/VotingSongResponse.java | 2 +- .../shook/voting_song/domain/VotingSong.java | 19 ++- .../ControllerAdviceTest.java | 9 +- .../song/application/SongServiceTest.java | 23 ++- .../{SingerTest.java => ArtistNameTest.java} | 20 +-- .../shook/shook/song/domain/SongTest.java | 24 ++- .../domain/killingpart/KillingPartTest.java | 11 +- .../repository/KillingPartRepositoryTest.java | 21 ++- .../domain/repository/SongRepositoryTest.java | 150 ++++++++++------ .../song/ui/AdminSongControllerTest.java | 33 +--- .../song/ui/HighLikedSongControllerTest.java | 2 + .../shook/song/ui/MyPageControllerTest.java | 17 +- .../VotingSongPartServiceTest.java | 44 +++-- .../application/VotingSongServiceTest.java | 81 +++++---- .../domain/VotingSongPartTest.java | 12 +- .../domain/VotingSongPartsTest.java | 12 +- .../voting_song/domain/VotingSongTest.java | 36 +++- .../domain/repository/VoteRepositoryTest.java | 25 ++- .../VotingSongPartRepositoryTest.java | 27 ++- .../repository/VotingSongRepositoryTest.java | 161 ++++++------------ .../ui/VotingSongControllerTest.java | 48 ++++-- .../ui/VotingSongPartControllerTest.java | 17 +- .../initialize_killing_part_song.sql | 32 +++- backend/src/test/resources/schema-test.sql | 14 +- 42 files changed, 834 insertions(+), 370 deletions(-) create mode 100644 backend/src/main/java/shook/shook/song/domain/Artist.java create mode 100644 backend/src/main/java/shook/shook/song/domain/ArtistName.java create mode 100644 backend/src/main/java/shook/shook/song/domain/ProfileImageUrl.java create mode 100644 backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java create mode 100644 backend/src/main/java/shook/shook/song/exception/ArtistException.java rename backend/src/test/java/shook/shook/song/domain/{SingerTest.java => ArtistNameTest.java} (62%) diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 0b77761d5..6dff25f27 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -53,6 +53,9 @@ 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자를 넘길 수 없습니다."), + // 4000: 투표 VOTING_PART_START_LESS_THAN_ZERO(4001, "파트의 시작 초는 0보다 작을 수 없습니다."), diff --git a/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java b/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java index 0779a4d19..5a7c99f08 100644 --- a/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java +++ b/backend/src/main/java/shook/shook/globalexception/GlobalExceptionHandler.java @@ -14,6 +14,7 @@ import shook.shook.auth.exception.TokenException; import shook.shook.member.exception.MemberException; 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; @@ -55,7 +56,8 @@ public ResponseEntity handleTokenException(final CustomException MemberException.class, VotingSongException.class, VotingSongPartException.PartNotExistException.class, - PartException.class + PartException.class, + ArtistException.class }) public ResponseEntity handleGlobalBadRequestException(final CustomException e) { log.error(e.getErrorInfoLog()); 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 2283a8181..c925f9b41 100644 --- a/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java +++ b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java @@ -16,8 +16,11 @@ import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.exception.SongDataFileReadException; @@ -91,7 +94,9 @@ 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 -> new Song(title, videoId, albumCoverUrl, + new Artist(new ProfileImageUrl("image"), new ArtistName("name")), length, + Genre.from(genre), parts)); } 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 1ae2b15fc..49dafd6a2 100644 --- a/backend/src/main/java/shook/shook/song/application/SongService.java +++ b/backend/src/main/java/shook/shook/song/application/SongService.java @@ -18,11 +18,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) @@ -38,6 +37,7 @@ public class SongService { private final KillingPartLikeRepository killingPartLikeRepository; private final MemberRepository memberRepository; private final InMemorySongs inMemorySongs; + private final ArtistRepository artistRepository; private final SongDataExcelReader songDataExcelReader; @Transactional @@ -48,9 +48,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; @@ -68,8 +66,10 @@ public SongSwipeResponse findSongByIdForFirstSwipe( ) { final Song currentSong = inMemorySongs.getSongById(songId); - final List beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT); - final List afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT); + final List beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, + BEFORE_SONGS_COUNT); + final List afterSongs = inMemorySongs.getNextLikedSongs(currentSong, + AFTER_SONGS_COUNT); return convertToSongSwipeResponse(memberInfo, currentSong, beforeSongs, afterSongs); } @@ -87,7 +87,8 @@ private SongSwipeResponse convertToSongSwipeResponse( } final Member member = findMemberById(memberInfo.getMemberId()); - final List killingPartIds = killingPartLikeRepository.findLikedKillingPartIdsByMember(member); + final List killingPartIds = killingPartLikeRepository.findLikedKillingPartIdsByMember( + member); return SongSwipeResponse.of(currentSong, beforeSongs, afterSongs, killingPartIds); } @@ -105,7 +106,8 @@ public List findSongByIdForBeforeSwipe( final MemberInfo memberInfo ) { final Song currentSong = inMemorySongs.getSongById(songId); - final List beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT); + final List beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, + BEFORE_SONGS_COUNT); return convertToSongResponses(memberInfo, beforeSongs); } @@ -136,7 +138,8 @@ public List findSongByIdForAfterSwipe( final MemberInfo memberInfo ) { final Song currentSong = inMemorySongs.getSongById(songId); - final List afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT); + final List afterSongs = inMemorySongs.getNextLikedSongs(currentSong, + AFTER_SONGS_COUNT); return convertToSongResponses(memberInfo, afterSongs); } @@ -163,7 +166,8 @@ public SongSwipeResponse findSongsByGenreForSwipe( final Song currentSong = inMemorySongs.getSongById(songId); final List prevSongs = inMemorySongs.getPrevLikedSongByGenre(currentSong, genre, BEFORE_SONGS_COUNT); - final List nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre, AFTER_SONGS_COUNT); + final List nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre, + AFTER_SONGS_COUNT); final Authority authority = memberInfo.getAuthority(); @@ -197,7 +201,8 @@ public List findNextSongsByGenre( ) { final Genre genre = Genre.findByName(genreName); final Song currentSong = inMemorySongs.getSongById(songId); - final List nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre, AFTER_SONGS_COUNT); + final List nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre, + AFTER_SONGS_COUNT); return convertToSongResponses(memberInfo, nextSongs); } 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/SongResponse.java b/backend/src/main/java/shook/shook/song/application/dto/SongResponse.java index dc453c70a..eb70fb384 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 @@ -43,7 +43,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/SongWithKillingPartsRegisterRequest.java b/backend/src/main/java/shook/shook/song/application/dto/SongWithKillingPartsRegisterRequest.java index 58494018e..109bc025c 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,8 +10,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; @Schema(description = "노래와 킬링파트 등록 요청") @@ -34,7 +37,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 +57,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(new ProfileImageUrl(profileImageUrl), new ArtistName(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..88d181e54 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -0,0 +1,70 @@ +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 ProfileImageUrl profileImageUrl, final ArtistName artistName) { + this.profileImageUrl = profileImageUrl; + this.artistName = artistName; + } + + public String getArtistName() { + return artistName.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..e6830ea2f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.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 ArtistName { + + private static final int NAME_MAXIMUM_LENGTH = 50; + + @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) + ); + } + } +} 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/repository/ArtistRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java new file mode 100644 index 000000000..e8c625a5e --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java @@ -0,0 +1,8 @@ +package shook.shook.song.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import shook.shook.song.domain.Artist; + +public interface ArtistRepository extends JpaRepository { + +} 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..4622ef3d3 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -0,0 +1,63 @@ +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); + } + } +} 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/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..4e4189dab 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,9 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.domain.VotingSong; @Schema(description = "파트 수집 중인 노래 등록 요청") @@ -30,7 +33,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 +45,11 @@ public class VotingSongRegisterRequest { private Integer length; public VotingSong getVotingSong() { - return new VotingSong(title, videoId, imageUrl, singer, length); + final Artist artist = new Artist( + new ProfileImageUrl(profileImageUrl), + new ArtistName(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/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/song/application/SongServiceTest.java b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java index ffb5a9159..4c29646b8 100644 --- a/backend/src/test/java/shook/shook/song/application/SongServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java @@ -26,6 +26,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; @@ -45,6 +46,9 @@ class SongServiceTest extends UsingJpaTest { @Autowired private MemberRepository memberRepository; + @Autowired + private ArtistRepository artistRepository; + private final InMemorySongs inMemorySongs = new InMemorySongs(); private SongService songService; @@ -57,6 +61,7 @@ public void setUp() { likeRepository, memberRepository, inMemorySongs, + artistRepository, new SongDataExcelReader(" ", " ", " ") ); } @@ -66,7 +71,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), @@ -86,7 +97,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) ); @@ -226,7 +237,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), diff --git a/backend/src/test/java/shook/shook/song/domain/SingerTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java similarity index 62% rename from backend/src/test/java/shook/shook/song/domain/SingerTest.java rename to backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index 31fedd8b2..57bf851d5 100644 --- a/backend/src/test/java/shook/shook/song/domain/SingerTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -8,9 +8,9 @@ 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; +import shook.shook.song.exception.ArtistException; -class SingerTest { +class ArtistNameTest { @DisplayName("가수을 뜻하는 객체를 생성한다.") @Test @@ -18,7 +18,7 @@ void create_success() { //given //when //then - Assertions.assertDoesNotThrow(() -> new Singer("이름")); + Assertions.assertDoesNotThrow(() -> new ArtistName("이름")); } @DisplayName("가수 이름이 유효하지 않으면 예외를 던진다.") @@ -29,19 +29,19 @@ void create_fail_lessThanOne(final String name) { //given //when //then - assertThatThrownBy(() -> new Singer(name)) - .isInstanceOf(SongException.NullOrEmptySingerNameException.class); + assertThatThrownBy(() -> new ArtistName(name)) + .isInstanceOf(ArtistException.NullOrEmptyNameException.class); } - @DisplayName("가수 이름의 길이가 100을 넘을 경우 예외를 던진다.") + @DisplayName("가수 이름의 길이가 50을 넘을 경우 예외를 던진다.") @Test - void create_fail_lengthOver100() { + void create_fail_lengthOver50() { //given - final String name = ".".repeat(101); + final String name = ".".repeat(51); //when //then - assertThatThrownBy(() -> new Singer(name)) - .isInstanceOf(SongException.TooLongSingerNameException.class); + assertThatThrownBy(() -> new ArtistName(name)) + .isInstanceOf(ArtistException.TooLongNameException.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 afa61816d..25cc40e19 100644 --- a/backend/src/test/java/shook/shook/song/domain/SongTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SongTest.java @@ -18,8 +18,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(new ProfileImageUrl("image"), new ArtistName("name")), + 300, + Genre.from("댄스"), + null + ) + ).isInstanceOf(KillingPartsException.EmptyKillingPartsException.class); } @DisplayName("Song 의 KillingPart 시작 시간, 종료 시간이 지정된 재생 가능한 URL 을 반환한다.") @@ -32,9 +40,17 @@ void getPartVideoUrl() { final KillingParts killingParts = new KillingParts( List.of(killingPart1, killingPart2, killingPart3) ); - final Song song = new Song("title", "3rUPND6FG8A", "image_url", "singer", 230, + + final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Song song = new Song( + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, Genre.from("댄스"), - killingParts); + killingParts + ); // when final String killingPart1VideoUrl = song.getPartVideoUrl(killingPart1); 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 1b9b75913..df1bc39ae 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 @@ -9,8 +9,11 @@ import org.junit.jupiter.api.Test; import shook.shook.member.domain.Member; import shook.shook.part.domain.PartLength; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.exception.SongException; import shook.shook.song.exception.killingpart.KillingPartCommentException; @@ -141,7 +144,13 @@ void setSong_alreadyRegisteredToSong_fail() { final KillingPart dummyKillingPart1 = KillingPart.forSave(0, PartLength.STANDARD); final KillingPart dummyKillingPart2 = KillingPart.forSave(0, PartLength.SHORT); final KillingPart dummyKillingPart3 = KillingPart.forSave(0, PartLength.LONG); - final Song song = new Song("title", "elevenVideo", "imageUrl", "singer", 10, + final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Song song = new Song( + "title", + "3rUPND6FG8A", + "image_url", + artist, + 230, Genre.from("댄스"), new KillingParts(List.of(dummyKillingPart1, dummyKillingPart2, dummyKillingPart3)) ); 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 c04d65933..4c56324c3 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 @@ -10,10 +10,14 @@ 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.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; 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; @@ -28,6 +32,9 @@ class KillingPartRepositoryTest extends UsingJpaTest { @Autowired private KillingPartRepository killingPartRepository; + @Autowired + private ArtistRepository artistRepository; + @Autowired private SongRepository songRepository; @@ -43,8 +50,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(new ProfileImageUrl("image"), new ArtistName("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 를 모두 저장한다.") 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 55d06b5f8..32bf0dd6b 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 @@ -13,8 +13,11 @@ 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.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.domain.killingpart.KillingPartLike; @@ -37,14 +40,24 @@ class SongRepositoryTest extends UsingJpaTest { @Autowired private MemberRepository memberRepository; + @Autowired + private ArtistRepository artistRepository; + private Song createNewSongWithKillingParts() { final KillingPart firstKillingPart = KillingPart.forSave(10, PartLength.SHORT); final KillingPart secondKillingPart = KillingPart.forSave(15, PartLength.SHORT); final KillingPart thirdKillingPart = KillingPart.forSave(20, PartLength.SHORT); + final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("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) { @@ -59,19 +72,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 @@ -80,8 +98,7 @@ void findById() { //then assertThat(findSong).isPresent(); - assertThat(findSong.get()).usingRecursiveComparison() - .isEqualTo(song); + assertThat(findSong.get()).isEqualTo(song); } @DisplayName("Song 을 저장할 때의 시간 정보로 createAt이 자동 생성된다.") @@ -92,7 +109,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 @@ -106,9 +123,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()); @@ -142,17 +159,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()); @@ -185,7 +202,7 @@ void findSongsWithLessLikeCountThanSongWithId() { addLikeToKillingPart(fifthSong.getKillingParts().get(1), member); // when - saveAndClearEntityManager(); + saveAndClearEntityManager(); // artist, song, killingPart 다 떼어버림 final List songs = songRepository.findSongsWithLessLikeCountThanSongWithId( standardSong.getId(), PageRequest.of(0, 10) @@ -194,22 +211,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()); @@ -244,7 +275,7 @@ void findSongsWithLessLikeCountThanSongWithId_SmallData() { // then assertThat(songs).usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo(List.of(thirdSong)); + .isEqualTo(List.of(findSavedSong(thirdSong))); } @DisplayName("주어진 id보다 좋아요가 많은 노래 10개를 총 좋아요 오름차순, id 오름차순으로 조회한다. (데이터가 충분할 때)") @@ -252,17 +283,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()); @@ -296,6 +327,7 @@ void findSongsWithMoreLikeCountThanSongWithId() { // when saveAndClearEntityManager(); + final List songs = songRepository.findSongsWithMoreLikeCountThanSongWithId( standardSong.getId(), PageRequest.of(0, 10) @@ -304,9 +336,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 오름차순으로 조회한다. (데이터가 기준보다 부족할 때)") @@ -314,12 +355,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()); @@ -356,7 +397,12 @@ 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)) + ); } } 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/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 bf5803e7c..9653bde7d 100644 --- a/backend/src/test/java/shook/shook/song/ui/MyPageControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/MyPageControllerTest.java @@ -106,13 +106,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) @@ -122,7 +115,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/voting_song/application/VotingSongPartServiceTest.java b/backend/src/test/java/shook/shook/voting_song/application/VotingSongPartServiceTest.java index 6a6a9b55d..badd35363 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +46,9 @@ class VotingSongPartServiceTest extends UsingJpaTest { @Autowired private VoteRepository voteRepository; + @Autowired + private ArtistRepository artistRepository; + private VotingSongPartService votingSongPartService; @BeforeEach @@ -54,7 +61,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + artistRepository.save(artist); + SAVED_SONG = votingSongRepository.save(new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180) + ); } void addPart(final VotingSong votingSong, final VotingSongPart votingSongPart) { @@ -79,11 +94,13 @@ void notRegistered() { final MemberInfo memberInfo = new MemberInfo(FIRST_MEMBER.getId(), Authority.MEMBER); //when - votingSongPartService.registerAndReturnMemberPartDuplication(memberInfo, SAVED_SONG.getId(), request); + votingSongPartService.registerAndReturnMemberPartDuplication(memberInfo, + SAVED_SONG.getId(), request); saveAndClearEntityManager(); //then - final List votingSongs = votingSongPartRepository.findAllByVotingSong(SAVED_SONG); + final List votingSongs = votingSongPartRepository.findAllByVotingSong( + SAVED_SONG); assertThat(votingSongs).hasSize(1); assertThat(votingSongs.get(0).getVoteCount()).isOne(); } @@ -92,7 +109,8 @@ void notRegistered() { @Test void registered_membersSamePartExist() { //given - final VotingSongPart votingSongPart = VotingSongPart.forSave(1, PartLength.SHORT, SAVED_SONG); + final VotingSongPart votingSongPart = VotingSongPart.forSave(1, PartLength.SHORT, + SAVED_SONG); addPart(SAVED_SONG, votingSongPart); final Vote vote = Vote.forSave(FIRST_MEMBER, votingSongPart); @@ -101,8 +119,10 @@ void registered_membersSamePartExist() { final VotingSongPartRegisterRequest request = new VotingSongPartRegisterRequest(1, 5); //when - final MemberInfo anotherMemberInfo = new MemberInfo(FIRST_MEMBER.getId(), Authority.MEMBER); - votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, SAVED_SONG.getId(), + final MemberInfo anotherMemberInfo = new MemberInfo(FIRST_MEMBER.getId(), + Authority.MEMBER); + votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, + SAVED_SONG.getId(), request); saveAndClearEntityManager(); @@ -117,7 +137,8 @@ void registered_membersSamePartExist() { @Test void registered() { //given - final VotingSongPart votingSongPart = VotingSongPart.forSave(1, PartLength.SHORT, SAVED_SONG); + final VotingSongPart votingSongPart = VotingSongPart.forSave(1, PartLength.SHORT, + SAVED_SONG); addPart(SAVED_SONG, votingSongPart); final Vote vote = Vote.forSave(FIRST_MEMBER, votingSongPart); @@ -126,8 +147,10 @@ void registered() { final VotingSongPartRegisterRequest request = new VotingSongPartRegisterRequest(1, 5); //when - final MemberInfo anotherMemberInfo = new MemberInfo(SECOND_MEMBER.getId(), Authority.MEMBER); - votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, SAVED_SONG.getId(), + final MemberInfo anotherMemberInfo = new MemberInfo(SECOND_MEMBER.getId(), + Authority.MEMBER); + votingSongPartService.registerAndReturnMemberPartDuplication(anotherMemberInfo, + SAVED_SONG.getId(), request); saveAndClearEntityManager(); @@ -149,7 +172,8 @@ void songNotExist() { //when //then assertThatThrownBy( - () -> votingSongPartService.registerAndReturnMemberPartDuplication(memberInfo, notExistSongId, request)) + () -> votingSongPartService.registerAndReturnMemberPartDuplication(memberInfo, + notExistSongId, request)) .isInstanceOf(VotingSongException.VotingSongNotExistException.class); } } 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..763ddd1dd 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,11 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; 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 +26,46 @@ class VotingSongServiceTest extends UsingJpaTest { + public 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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +80,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 +93,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +133,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 12acacc4e..eba677d14 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,22 @@ 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.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; 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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 52e828f54..05c5c4e8e 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,9 @@ 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.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; class VotingSongPartsTest { @@ -16,7 +19,14 @@ class VotingSongPartsTest { @Test void create_fail_duplicatePartExist() { //given - final VotingSong votingSong = new VotingSong("제목", "비디오ID는 11글자", "이미지URL", "가수", 30); + final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final VotingSong votingSong = new VotingSong( + "제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 30 + ); final VotingSongPart firstPart = VotingSongPart.saved(1L, 5, PartLength.SHORT, votingSong); final VotingSongPart secondPart = VotingSongPart.forSave(5, PartLength.SHORT, 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 a544c11a8..593848366 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,32 @@ 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.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.exception.VotingSongPartException; class VotingSongTest { + private final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + @DisplayName("파트 수집 중인 노래에 파트를 등록한다. ( 노래에 해당하는 파트일 때 )") @Test void addPart_valid() { //given - final VotingSong votingSong = new VotingSong("노래제목", "비디오ID는 11글자", "이미지URL", "가수", 180); - final VotingSongPart votingSongPart = VotingSongPart.forSave(1, PartLength.STANDARD, votingSong); + final VotingSong votingSong = new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180 + ); + final VotingSongPart votingSongPart = VotingSongPart.forSave(1, PartLength.STANDARD, + votingSong); //when votingSong.addPart(votingSongPart); + final String artistName = votingSong.getArtistName(); //then assertThat(votingSong.getParts()).hasSize(1); @@ -28,9 +41,22 @@ 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 VotingSongPart partInSecondSong = VotingSongPart.forSave(1, PartLength.STANDARD, secondSong); + 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, PartLength.STANDARD, + secondSong); //when //then 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 ea100014e..536e6f799 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +33,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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, PartLength.SHORT, votingSong)); + VotingSongPart.forSave(1, PartLength.SHORT, 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 3e7bbdeb6..aa49a7da8 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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,19 +27,32 @@ 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(new ProfileImageUrl("profile"), new ArtistName("가수")); + artistRepository.save(artist); SAVED_SONG = votingSongRepository.save( - new VotingSong("제목", "비디오ID는 11글자", "이미지URL", "가수", 30)); + new VotingSong( + "제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 30) + ); } @DisplayName("VotingSongPart 를 저장한다.") @Test void save() { //given - final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, SAVED_SONG); + final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, + SAVED_SONG); //when final VotingSongPart saved = votingSongPartRepository.save(votingSongPart); @@ -49,7 +66,8 @@ void save() { @Test void createdAt() { //given - final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, SAVED_SONG); + final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, + SAVED_SONG); //when final LocalDateTime prev = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); @@ -72,7 +90,8 @@ void findAllBySong() { //when saveAndClearEntityManager(); - final List allBySong = votingSongPartRepository.findAllByVotingSong(SAVED_SONG); + final List allBySong = votingSongPartRepository.findAllByVotingSong( + SAVED_SONG); //then assertThat(allBySong).containsAll(List.of(firstPart, secondPart)); 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..9607d76a8 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -15,6 +19,23 @@ class VotingSongRepositoryTest extends UsingJpaTest { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +44,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 +83,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 +118,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 +153,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..2bd29a64b 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +25,29 @@ class VotingSongControllerTest extends AcceptanceTest { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +74,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 +109,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 +139,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..0e5963875 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +47,9 @@ void setUp() { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + @Autowired private VotingSongPartService votingSongPartService; @@ -89,7 +96,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 42e4f05be..f67a014b8 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; @@ -8,7 +9,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, @@ -61,17 +62,30 @@ create table if not exists killing_part_comment 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 2e6c81cbc..02facfdc8 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -6,12 +6,13 @@ 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; 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, @@ -59,9 +60,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) @@ -92,3 +93,12 @@ create table if not exists member created_at timestamp(6) not null, primary key (id) ); + +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) +); From 82fe9893b38a1a2d672cf15df6c0593c0e138bb3 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 10 Oct 2023 13:32:45 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20artist=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20song,=20voting=5Fs?= =?UTF-8?q?ong=20singer=20->=20artist=5Fid=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/dev/data.sql | 2 ++ backend/src/main/resources/dev/schema.sql | 15 +++++++++++++-- backend/src/main/resources/schema.sql | 14 ++++++++++++++ backend/src/test/resources/schema-test.sql | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index 620344dff..151522379 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -1,3 +1,5 @@ +-- 사용 불가한 data.sql + TRUNCATE TABLE song; TRUNCATE TABLE voting_song; diff --git a/backend/src/main/resources/dev/schema.sql b/backend/src/main/resources/dev/schema.sql index 6d216c192..2cea4f495 100644 --- a/backend/src/main/resources/dev/schema.sql +++ b/backend/src/main/resources/dev/schema.sql @@ -2,15 +2,17 @@ drop table if exists song; drop table if exists killing_part; drop table if exists killing_part_like; drop table if exists killing_part_comment; +drop table if exists voting_song_part; drop table if exists voting_song; drop table if exists vote; drop table if exists member; +drop table if exists artist; 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, @@ -58,9 +60,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) @@ -91,3 +93,12 @@ create table if not exists member 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) +); diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index c787967ca..0b4dbca99 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -94,3 +94,17 @@ 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; diff --git a/backend/src/test/resources/schema-test.sql b/backend/src/test/resources/schema-test.sql index 02facfdc8..2cea4f495 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -96,7 +96,7 @@ create table if not exists member create table if not exists artist ( - id bigint generated by default as identity, + id bigint auto_increment, name varchar(50) not null, profile_image_url text not null, created_at timestamp(6) not null, From df3248ed7c5b1529e51f45e5696c04c24a2d7bbd Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 10 Oct 2023 13:38:08 +0900 Subject: [PATCH 03/21] =?UTF-8?q?config:=20shook-security=20=EC=8A=A4?= =?UTF-8?q?=EB=83=85=EC=83=B7=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/shook-security | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index e721f503b..69b80958a 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit e721f503bfdb5990582baa440869e3b936849ba6 +Subproject commit 69b80958a68b012293b06cfb2f5f460267b8e8c7 From 73036d136546c4d377227cd68814d7648e99cd35 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 10 Oct 2023 17:00:14 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat:=20ArtistSynonym=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/globalexception/ErrorCode.java | 2 + .../shook/song/domain/ArtistSynonym.java | 62 +++++++++++++++++++ .../java/shook/shook/song/domain/Synonym.java | 39 ++++++++++++ .../shook/song/exception/ArtistException.java | 22 +++++++ backend/src/main/resources/dev/schema.sql | 9 +++ backend/src/main/resources/schema.sql | 8 +++ .../shook/song/domain/ArtistNameTest.java | 2 +- .../song/domain/ProfileImageUrlTest.java | 46 ++++++++++++++ .../shook/shook/song/domain/SynonymTest.java | 47 ++++++++++++++ backend/src/test/resources/schema-test.sql | 9 +++ 10 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java create mode 100644 backend/src/main/java/shook/shook/song/domain/Synonym.java create mode 100644 backend/src/test/java/shook/shook/song/domain/ProfileImageUrlTest.java create mode 100644 backend/src/test/java/shook/shook/song/domain/SynonymTest.java diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 6dff25f27..74be590af 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -55,6 +55,8 @@ public enum ErrorCode { 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자를 넘길 수 없습니다.."), // 4000: 투표 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..1822f5b4b --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java @@ -0,0 +1,62 @@ +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.LAZY) + @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) + private Artist artist; + + @Embedded + private Synonym synonym; + + public String getArtistName() { + return artist.getArtistName(); + } + + public String getSynonym() { + return synonym.getValue(); + } + + @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/Synonym.java b/backend/src/main/java/shook/shook/song/domain/Synonym.java new file mode 100644 index 000000000..7d9a68ac1 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/Synonym.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 Synonym { + + private static final int MAXIMUM_LENGTH = 255; + + @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) + ); + } + } +} diff --git a/backend/src/main/java/shook/shook/song/exception/ArtistException.java b/backend/src/main/java/shook/shook/song/exception/ArtistException.java index 4622ef3d3..e1a0cbd34 100644 --- a/backend/src/main/java/shook/shook/song/exception/ArtistException.java +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -60,4 +60,26 @@ 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); + } + } } diff --git a/backend/src/main/resources/dev/schema.sql b/backend/src/main/resources/dev/schema.sql index 2cea4f495..510d00504 100644 --- a/backend/src/main/resources/dev/schema.sql +++ b/backend/src/main/resources/dev/schema.sql @@ -7,6 +7,7 @@ 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 artist_synonym; create table if not exists song ( @@ -102,3 +103,11 @@ create table if not exists artist 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 0b4dbca99..9c3212d36 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -108,3 +108,11 @@ 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/song/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index 57bf851d5..b0770c3cc 100644 --- a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -12,7 +12,7 @@ class ArtistNameTest { - @DisplayName("가수을 뜻하는 객체를 생성한다.") + @DisplayName("가수 이름을 뜻하는 객체를 생성한다.") @Test void create_success() { //given 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/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java new file mode 100644 index 000000000..ae8eb7d21 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -0,0 +1,47 @@ +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.ArtistException; + +class SynonymTest { + + @DisplayName("가수 이름 동의어를 뜻하는 객체를 생성한다.") + @Test + void create_success() { + //given + //when + //then + Assertions.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); + } +} diff --git a/backend/src/test/resources/schema-test.sql b/backend/src/test/resources/schema-test.sql index 2cea4f495..510d00504 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -7,6 +7,7 @@ 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 artist_synonym; create table if not exists song ( @@ -102,3 +103,11 @@ create table if not exists artist 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) +); From f97321d19ab1b2a59b0cfdfaed90938509871519 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 12 Oct 2023 13:47:23 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat:=20=EA=B0=80=EC=88=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84,=20=EB=8F=99=EC=9D=98=EC=96=B4=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/globalexception/ErrorCode.java | 3 +- .../song/application/ArtistSearchService.java | 119 +++++++++ .../song/application/dto/ArtistResponse.java | 30 +++ .../dto/ArtistWithSongSearchResponse.java | 47 ++++ .../application/dto/SongSearchResponse.java | 30 +++ .../java/shook/shook/song/domain/Artist.java | 12 + .../shook/shook/song/domain/ArtistName.java | 29 +++ .../shook/song/domain/ArtistSynonym.java | 19 +- .../song/domain/InMemoryArtistSynonyms.java | 72 ++++++ .../InMemoryArtistSynonymsGenerator.java | 32 +++ .../java/shook/shook/song/domain/Synonym.java | 29 +++ .../domain/repository/ArtistRepository.java | 2 + .../repository/ArtistSynonymRepository.java | 10 + .../domain/repository/SongRepository.java | 9 + .../shook/song/exception/ArtistException.java | 11 + .../song/ui/ArtistSongSearchController.java | 44 ++++ .../song/ui/openapi/ArtistSongSearchApi.java | 78 ++++++ .../application/ArtistSearchServiceTest.java | 238 ++++++++++++++++++ .../shook/song/domain/ArtistNameTest.java | 47 ++++ .../domain/InMemoryArtistSynonymsTest.java | 164 ++++++++++++ .../shook/shook/song/domain/SynonymTest.java | 47 ++++ .../domain/repository/SongRepositoryTest.java | 36 +++ .../ui/ArtistSongSearchControllerTest.java | 153 +++++++++++ .../song/ui/SongSwipeControllerTest.java | 2 +- 24 files changed, 1256 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/shook/shook/song/application/ArtistSearchService.java create mode 100644 backend/src/main/java/shook/shook/song/application/dto/ArtistResponse.java create mode 100644 backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java create mode 100644 backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java create mode 100644 backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonyms.java create mode 100644 backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonymsGenerator.java create mode 100644 backend/src/main/java/shook/shook/song/domain/repository/ArtistSynonymRepository.java create mode 100644 backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java create mode 100644 backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java create mode 100644 backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java create mode 100644 backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java create mode 100644 backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 74be590af..be84048fd 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -56,7 +56,8 @@ public enum ErrorCode { EMPTY_ARTIST_PROFILE_URL(3014, "가수 프로필 이미지는 비어있을 수 없습니다."), TOO_LONG_ARTIST_PROFILE_URL(3015, "가수 프로필 이미지URL은 65,356자를 넘길 수 없습니다."), EMPTY_ARTIST_SYNONYM(3016, "가수 동의어는 비어있을 수 없습니다."), - TOO_LONG_ARTIST_SYNONYM(3017, "가수 동의어는 255자를 넘길 수 없습니다.."), + TOO_LONG_ARTIST_SYNONYM(3017, "가수 동의어는 255자를 넘길 수 없습니다."), + ARTIST_NOT_EXIST(3018, "존재하지 않는 가수입니다."), // 4000: 투표 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..be249fc04 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java @@ -0,0 +1,119 @@ +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 lombok.extern.slf4j.Slf4j; +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.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 +@Slf4j +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; + + 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 all = songRepository.findAll(); + log.info("all song: {}", all); + final List songsWithTotalLikeCount = songRepository.findAllSongsWithTotalLikeCountByArtist( + artist); + + log.info("found song of artist: {}", songsWithTotalLikeCount); + 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)) + )); + } +} 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..a495dce4f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java @@ -0,0 +1,47 @@ +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) + ); + } + + private static List convertToSongSearchResponse(final List songs) { + return songs.stream() + .map(SongSearchResponse::from) + .toList(); + } +} 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..7eb66a45a --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.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.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; + + public static SongSearchResponse from(final Song song) { + return new SongSearchResponse(song.getId(), song.getTitle(), song.getAlbumCoverUrl(), + song.getLength()); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/Artist.java b/backend/src/main/java/shook/shook/song/domain/Artist.java index 88d181e54..7e7412247 100644 --- a/backend/src/main/java/shook/shook/song/domain/Artist.java +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -44,10 +44,22 @@ public Artist(final ProfileImageUrl profileImageUrl, final ArtistName artistName this.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) { diff --git a/backend/src/main/java/shook/shook/song/domain/ArtistName.java b/backend/src/main/java/shook/shook/song/domain/ArtistName.java index e6830ea2f..d3893a7e1 100644 --- a/backend/src/main/java/shook/shook/song/domain/ArtistName.java +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.java @@ -17,6 +17,7 @@ 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; @@ -36,4 +37,32 @@ private void validateName(final String value) { ); } } + + public boolean startsWithIgnoringCaseAndWhiteSpace(final String keyword) { + final String targetKeyword = toLowerCaseRemovingWhiteSpace(keyword); + if (StringChecker.isNullOrBlank(targetKeyword)) { + return false; + } + + return toLowerCaseRemovingWhiteSpace(value) + .startsWith(toLowerCaseRemovingWhiteSpace(keyword)); + } + + 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 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/ArtistSynonym.java b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java index 1822f5b4b..6fb63223f 100644 --- a/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java +++ b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java @@ -25,19 +25,28 @@ public class ArtistSynonym { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) private Artist artist; @Embedded private Synonym synonym; - public String getArtistName() { - return artist.getArtistName(); + 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 String getSynonym() { - return synonym.getValue(); + public boolean endsWith(final String keyword) { + return synonym.endsWithIgnoringCaseAndWhiteSpace(keyword); + } + + public String getArtistName() { + return artist.getArtistName(); } @Override 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/Synonym.java b/backend/src/main/java/shook/shook/song/domain/Synonym.java index 7d9a68ac1..450e197a3 100644 --- a/backend/src/main/java/shook/shook/song/domain/Synonym.java +++ b/backend/src/main/java/shook/shook/song/domain/Synonym.java @@ -17,6 +17,7 @@ public class Synonym { private static final int MAXIMUM_LENGTH = 255; + private static final String BLANK = "\\s"; @Column(name = "synonym", nullable = false) private String value; @@ -36,4 +37,32 @@ private void validate(final String 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 index e8c625a5e..ded9e3822 100644 --- a/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java +++ b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java @@ -1,8 +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 index e1a0cbd34..ee73f0188 100644 --- a/backend/src/main/java/shook/shook/song/exception/ArtistException.java +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -82,4 +82,15 @@ 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/ui/ArtistSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java new file mode 100644 index 000000000..d7128995f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java @@ -0,0 +1,44 @@ +package shook.shook.song.ui; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.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.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.ui.openapi.ArtistSongSearchApi; + +@RequiredArgsConstructor +@RequestMapping("/singers") +@RestController +@Slf4j +public class ArtistSongSearchController implements ArtistSongSearchApi { + + private final ArtistSearchService artistSearchService; + + @GetMapping(params = {"name", "search=singer,song"}) + public ResponseEntity> searchArtistWithSongByKeyword( + @RequestParam(name = "search") final List searchTypes, + @RequestParam(name = "name") final String name) { + return ResponseEntity.ok(artistSearchService.searchArtistsAndTopSongsByKeyword(name)); + } + + @GetMapping(params = {"name", "search=singer"}) + public ResponseEntity> searchArtistByKeyword( + @RequestParam(name = "search") final String search, + @RequestParam(name = "name") final String name) { + return ResponseEntity.ok(artistSearchService.searchArtistsByKeyword(name)); + } + + @GetMapping("/{artist_id}") + public ResponseEntity searchSongsByArtist( + @PathVariable(name = "artist_id") final Long artistId) { + return ResponseEntity.ok(artistSearchService.searchAllSongsByArtist(artistId)); + } +} diff --git a/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java b/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java new file mode 100644 index 000000000..23be2bdee --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java @@ -0,0 +1,78 @@ +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.media.Schema; +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.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import shook.shook.song.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; + +@Tag(name = "Artist Search", description = "가수 이름 검색 API") +public interface ArtistSongSearchApi { + + @Operation( + summary = "검색어 입력 시 자동 완성되는 가수의 정보 검색", + description = "검색어 입력 시, 검색어로 시작하는 가수의 정보를 검색한다." + ) + @ApiResponse( + responseCode = "200", + description = "가수 검색 성공" + ) + @GetMapping(value = "?search=artist", params = {"name"}) + ResponseEntity> searchArtistByKeyword( + @Parameter(name = "search", description = "검색 타입", + schema = @Schema(enumAsRef = true, allowableValues = {"artist"})) + @RequestParam(name = "search") final String search, + @Parameter( + name = "name", + description = "검색할 가수 키워드", + required = true + ) + @RequestParam(name = "name") final String name + ); + + @Operation( + summary = "검색 시, 검색 결과 조회", + description = "검색 시, 검색어로 시작하거나 끝나는 가수와 해당 가수의 TOP3 노래가 조회된다." + ) + @ApiResponse( + responseCode = "200", + description = "가수, TOP3 노래 검색 성공" + ) + @GetMapping(value = "?search=artist,song", params = {"name"}) + ResponseEntity> searchArtistWithSongByKeyword( + @Parameter(name = "search", description = "검색 타입", + schema = @Schema(enumAsRef = true, allowableValues = {"artist", "song"})) + @RequestParam(name = "search") final List searchTypes, + @Parameter( + name = "name", + description = "검색할 가수 키워드", + required = true + ) + @RequestParam(name = "name") final String name + ); + + @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/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..36230cff2 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java @@ -0,0 +1,238 @@ +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.part.domain.PartLength; +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.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.SongRepository; +import shook.shook.song.exception.ArtistException; +import shook.shook.support.UsingJpaTest; + +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +class ArtistSearchServiceTest extends UsingJpaTest { + + private ArtistSearchService artistSearchService; + private InMemoryArtistSynonyms artistSynonyms = new InMemoryArtistSynonyms(); + + @Autowired + private SongRepository songRepository; + + @Autowired + private ArtistRepository artistRepository; + + + @Autowired + private KillingPartLikeRepository likeRepository; + + @Autowired + private KillingPartRepository killingPartRepository; + + @Autowired + private MemberRepository memberRepository; + + @BeforeEach + void setUp() { + artistSearchService = new ArtistSearchService(artistSynonyms, artistRepository, + songRepository); + 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, PartLength.SHORT); + final KillingPart secondKillingPart = KillingPart.forSave(15, PartLength.SHORT); + final KillingPart thirdKillingPart = KillingPart.forSave(20, PartLength.SHORT); + + 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/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index b0770c3cc..5bb90c9f4 100644 --- a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -1,11 +1,13 @@ package shook.shook.song.domain; +import static org.assertj.core.api.Assertions.assertThat; 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.CsvSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import shook.shook.song.exception.ArtistException; @@ -44,4 +46,49 @@ void create_fail_lengthOver50() { 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..f048698ac --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java @@ -0,0 +1,164 @@ +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(new ProfileImageUrl("image"), new ArtistName("name1")); + artist2 = new Artist(new ProfileImageUrl("image"), new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java index ae8eb7d21..5e06baf9d 100644 --- a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -1,11 +1,13 @@ package shook.shook.song.domain; +import static org.assertj.core.api.Assertions.assertThat; 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.CsvSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import shook.shook.song.exception.ArtistException; @@ -44,4 +46,49 @@ void create_fail_lengthOver255() { 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/repository/SongRepositoryTest.java b/backend/src/test/java/shook/shook/song/domain/repository/SongRepositoryTest.java index 32bf0dd6b..809098fb3 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; @@ -405,4 +406,39 @@ void findSongsWithMoreLikeCountThanSongWithId_smallData() { 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/ArtistSongSearchControllerTest.java b/backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java new file mode 100644 index 000000000..0166163eb --- /dev/null +++ b/backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java @@ -0,0 +1,153 @@ +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; +import shook.shook.song.domain.repository.ArtistSynonymRepository; + +@SuppressWarnings("NonAsciiCharacters") +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ArtistSongSearchControllerTest { + + 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 ArtistSynonymRepository synonymRepository; + + @Autowired + private ArtistRepository artistRepository; + + @DisplayName("search=singer,song name=검색어 으로 요청을 보내는 경우 상태코드 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("name", keyword, "search", searchType)) + .when().log().all() + .get("/singers") + .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("search=singer name=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하는 가수 목록을 반환한다.") + @Test + void searchArtistByKeyword() { + // given + final String searchType = "singer"; + final String keyword = "인기"; + + // when + final List response = RestAssured.given().log().all() + .params(Map.of("name", keyword, "search", searchType)) + .when().log().all() + .get("/singers") + .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()) + ); + } + + @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); + } +} 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 8adbbe9f2..7b2a02997 100644 --- a/backend/src/test/java/shook/shook/song/ui/SongSwipeControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/SongSwipeControllerTest.java @@ -23,6 +23,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 { @@ -251,7 +252,6 @@ void showPrevSongsWithGenre() { .extract() .body().jsonPath().getList(".", SongResponse.class); - //then //then assertThat(response.stream() .map(SongResponse::getId).toList()) From 6cb2fe0654293638f02109715573732b51029775 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 17 Oct 2023 14:34:35 +0900 Subject: [PATCH 06/21] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../song/application/ArtistSearchService.java | 5 ----- .../song/application/SongDataExcelReader.java | 4 +--- .../SongWithKillingPartsRegisterRequest.java | 4 +--- .../java/shook/shook/song/domain/Artist.java | 6 +++--- .../shook/shook/song/domain/ArtistName.java | 8 ++++---- .../song/domain/repository/SongRepository.java | 3 +++ .../song/ui/ArtistSongSearchController.java | 2 -- .../dto/VotingSongRegisterRequest.java | 9 ++------- .../shook/shook/song/domain/ArtistNameTest.java | 4 ++-- .../song/domain/InMemoryArtistSynonymsTest.java | 16 ++++++---------- .../java/shook/shook/song/domain/SongTest.java | 4 ++-- .../shook/shook/song/domain/SynonymTest.java | 4 ++-- .../domain/killingpart/KillingPartTest.java | 4 +--- .../repository/KillingPartRepositoryTest.java | 17 +++++++---------- .../domain/repository/SongRepositoryTest.java | 8 +++----- .../application/VotingSongPartServiceTest.java | 4 +--- .../application/VotingSongServiceTest.java | 9 ++++----- .../voting_song/domain/VotingSongPartTest.java | 4 +--- .../voting_song/domain/VotingSongPartsTest.java | 4 +--- .../voting_song/domain/VotingSongTest.java | 4 +--- .../domain/repository/VoteRepositoryTest.java | 4 +--- .../VotingSongPartRepositoryTest.java | 4 +--- .../repository/VotingSongRepositoryTest.java | 4 +--- .../ui/VotingSongControllerTest.java | 4 +--- .../ui/VotingSongPartControllerTest.java | 4 +--- 25 files changed, 50 insertions(+), 93 deletions(-) diff --git a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java index be249fc04..c38bb94cf 100644 --- a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java +++ b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shook.shook.song.application.dto.ArtistResponse; @@ -21,7 +20,6 @@ @RequiredArgsConstructor @Transactional(readOnly = true) @Service -@Slf4j public class ArtistSearchService { private static final int TOP_SONG_COUNT_OF_ARTIST = 3; @@ -87,12 +85,9 @@ private List getTopSongsOfArtist(final Artist artist) { } private List getSongsOfArtistSortedByLikeCount(final Artist artist) { - final List all = songRepository.findAll(); - log.info("all song: {}", all); final List songsWithTotalLikeCount = songRepository.findAllSongsWithTotalLikeCountByArtist( artist); - log.info("found song of artist: {}", songsWithTotalLikeCount); return songsWithTotalLikeCount.stream() .sorted(Comparator.comparing(SongTotalLikeCountDto::getTotalLikeCount, Comparator.reverseOrder()) 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 c925f9b41..2655ac987 100644 --- a/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java +++ b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java @@ -17,10 +17,8 @@ import org.springframework.web.multipart.MultipartFile; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.exception.SongDataFileReadException; @@ -95,7 +93,7 @@ private Optional parseToSong(final Row currentRow) { return killingParts.map( parts -> new Song(title, videoId, albumCoverUrl, - new Artist(new ProfileImageUrl("image"), new ArtistName("name")), length, + new Artist("image", "name"), length, Genre.from(genre), parts)); } 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 109bc025c..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 @@ -11,10 +11,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; @Schema(description = "노래와 킬링파트 등록 요청") @@ -61,7 +59,7 @@ public Song convertToSong() { title, videoId, imageUrl, - new Artist(new ProfileImageUrl(profileImageUrl), new ArtistName(artistName)), + new Artist(profileImageUrl, artistName), length, Genre.from(genre), convertToKillingParts() diff --git a/backend/src/main/java/shook/shook/song/domain/Artist.java b/backend/src/main/java/shook/shook/song/domain/Artist.java index 7e7412247..67eada87a 100644 --- a/backend/src/main/java/shook/shook/song/domain/Artist.java +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -39,9 +39,9 @@ private void prePersist() { createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); } - public Artist(final ProfileImageUrl profileImageUrl, final ArtistName artistName) { - this.profileImageUrl = profileImageUrl; - this.artistName = artistName; + public Artist(final String profileImageUrl, final String artistName) { + this.profileImageUrl = new ProfileImageUrl(profileImageUrl); + this.artistName = new ArtistName(artistName); } public boolean nameStartsWith(final String keyword) { diff --git a/backend/src/main/java/shook/shook/song/domain/ArtistName.java b/backend/src/main/java/shook/shook/song/domain/ArtistName.java index d3893a7e1..a3198e596 100644 --- a/backend/src/main/java/shook/shook/song/domain/ArtistName.java +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.java @@ -48,6 +48,10 @@ public boolean startsWithIgnoringCaseAndWhiteSpace(final String keyword) { .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)) { @@ -58,10 +62,6 @@ public boolean endsWithIgnoringCaseAndWhiteSpace(final String keyword) { .endsWith(toLowerCaseRemovingWhiteSpace(keyword)); } - 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/SongRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java index a0c09ce1f..bad8cd512 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 @@ -31,6 +31,9 @@ public interface SongRepository extends JpaRepository { + "HAVING SUM(COALESCE(kp.likeCount, 0)) < (SELECT SUM(COALESCE(kp2.likeCount, 0)) FROM KillingPart kp2 WHERE kp2.song.id = :id) " + "OR (SUM(COALESCE(kp.likeCount, 0)) = (SELECT SUM(COALESCE(kp3.likeCount, 0)) FROM KillingPart kp3 WHERE kp3.song.id = :id) AND s.id < :id) " + "ORDER BY SUM(COALESCE(kp.likeCount, 0)) DESC, s.id DESC") + // id 로 song 찾아온느 쿼리 1개 -> 비즈니스 로직에서 조건 필터링 => 2번을 1번으로 + // 100개 이하의 데이터는 비즈니스 로직에서 정렬하는 것을 추천한다. + // 조건이 확실하게 있는 경우는 쿼리에서 하는 것이 좋다. List findSongsWithLessLikeCountThanSongWithId( @Param("id") final Long songId, final Pageable pageable diff --git a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java index d7128995f..16cfc36b2 100644 --- a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java +++ b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java @@ -2,7 +2,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,7 +16,6 @@ @RequiredArgsConstructor @RequestMapping("/singers") @RestController -@Slf4j public class ArtistSongSearchController implements ArtistSongSearchApi { private final ArtistSearchService artistSearchService; 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 4e4189dab..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 @@ -9,8 +9,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.domain.VotingSong; @Schema(description = "파트 수집 중인 노래 등록 요청") @@ -45,11 +43,8 @@ public class VotingSongRegisterRequest { private Integer length; public VotingSong getVotingSong() { - final Artist artist = new Artist( - new ProfileImageUrl(profileImageUrl), - new ArtistName(artistName) - ); - + final Artist artist = new Artist(profileImageUrl, artistName); + return new VotingSong(title, videoId, imageUrl, artist, length); } } diff --git a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index 5bb90c9f4..ffb551f61 100644 --- a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -2,8 +2,8 @@ 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.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -20,7 +20,7 @@ void create_success() { //given //when //then - Assertions.assertDoesNotThrow(() -> new ArtistName("이름")); + assertDoesNotThrow(() -> new ArtistName("이름")); } @DisplayName("가수 이름이 유효하지 않으면 예외를 던진다.") diff --git a/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java index f048698ac..f5b598d5c 100644 --- a/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java +++ b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java @@ -36,8 +36,8 @@ class InMemoryArtistSynonymsTest { @BeforeEach void setUp() { - artist1 = new Artist(new ProfileImageUrl("image"), new ArtistName("name1")); - artist2 = new Artist(new ProfileImageUrl("image"), new ArtistName("name2")); + artist1 = new Artist("image", "name1"); + artist2 = new Artist("image", "name2"); artistRepository.saveAll(List.of(artist1, artist2)); synonym1 = new ArtistSynonym(artist1, new Synonym("synonym1")); @@ -62,8 +62,7 @@ void generator_initialize() { @Test void findAllArtistsHavingSynonymStartsOrEndsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newTestSy")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); @@ -82,8 +81,7 @@ void findAllArtistsHavingSynonymStartsOrEndsWith() { @Test void findAllArtistsHavingSynonymStartsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newTestSy")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); @@ -114,8 +112,7 @@ void findAllArtistsHavingSynonymStartsOrEndsWith_emptyInput() { @Test void findAllArtistsNameStartsOrEndsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newSynonym")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); @@ -134,8 +131,7 @@ void findAllArtistsNameStartsOrEndsWith() { @Test void findAllArtistsNameStartsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newSynonym")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); 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 25cc40e19..917904117 100644 --- a/backend/src/test/java/shook/shook/song/domain/SongTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SongTest.java @@ -22,7 +22,7 @@ void songCreate_nullKillingParts_fail() { "title", "videoId", "imageUrl", - new Artist(new ProfileImageUrl("image"), new ArtistName("name")), + new Artist("image", "name"), 300, Genre.from("댄스"), null @@ -41,7 +41,7 @@ void getPartVideoUrl() { List.of(killingPart1, killingPart2, killingPart3) ); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); final Song song = new Song( "title", "3rUPND6FG8A", diff --git a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java index 5e06baf9d..dee658b31 100644 --- a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -2,8 +2,8 @@ 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.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -20,7 +20,7 @@ void create_success() { //given //when //then - Assertions.assertDoesNotThrow(() -> new Synonym("동의어")); + assertDoesNotThrow(() -> new Synonym("동의어")); } @DisplayName("가수 이름 동의어가 유효하지 않으면 예외를 던진다.") 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 df1bc39ae..f1ad72fb6 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 @@ -10,10 +10,8 @@ import shook.shook.member.domain.Member; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.exception.SongException; import shook.shook.song.exception.killingpart.KillingPartCommentException; @@ -144,7 +142,7 @@ void setSong_alreadyRegisteredToSong_fail() { final KillingPart dummyKillingPart1 = KillingPart.forSave(0, PartLength.STANDARD); final KillingPart dummyKillingPart2 = KillingPart.forSave(0, PartLength.SHORT); final KillingPart dummyKillingPart3 = KillingPart.forSave(0, PartLength.LONG); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); final Song song = new Song( "title", "3rUPND6FG8A", 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 4c56324c3..392719ed2 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 @@ -11,10 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.domain.repository.ArtistRepository; @@ -50,7 +48,7 @@ void setUp() { THIRD_KILLING_PART ) ); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); final Song song = new Song( "title", "3rUPND6FG8A", @@ -73,13 +71,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 809098fb3..5c35775f9 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 @@ -15,10 +15,8 @@ import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.domain.killingpart.KillingPartLike; @@ -49,7 +47,7 @@ private Song createNewSongWithKillingParts() { final KillingPart secondKillingPart = KillingPart.forSave(15, PartLength.SHORT); final KillingPart thirdKillingPart = KillingPart.forSave(20, PartLength.SHORT); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); return new Song( "title", "3rUPND6FG8A", @@ -98,8 +96,8 @@ void findById() { final Optional findSong = songRepository.findById(song.getId()); //then - assertThat(findSong).isPresent(); - assertThat(findSong.get()).isEqualTo(song); + assertThat(findSong).isPresent() + .get().isEqualTo(song); } @DisplayName("Song 을 저장할 때의 시간 정보로 createAt이 자동 생성된다.") 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 badd35363..0b90f85d1 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 @@ -15,8 +15,6 @@ import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.application.dto.VotingSongPartRegisterRequest; @@ -61,7 +59,7 @@ void setUp() { ); FIRST_MEMBER = memberRepository.save(new Member("a@a.com", "nickname")); SECOND_MEMBER = memberRepository.save(new Member("b@b.com", "nickname")); - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); SAVED_SONG = votingSongRepository.save(new VotingSong( "노래제목", 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 763ddd1dd..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 @@ -12,8 +12,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.SongTitle; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; @@ -26,7 +24,8 @@ class VotingSongServiceTest extends UsingJpaTest { - public static final String VIDEO_ID = "비디오ID는 11글자"; + private static final String VIDEO_ID = "비디오ID는 11글자"; + @Autowired private VotingSongRepository votingSongRepository; @@ -41,7 +40,7 @@ void setUp() { } private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( votingSongTitle, VIDEO_ID, @@ -93,7 +92,7 @@ class findAll { @Test void findAllVotingSongs() { // given - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong firstSong = saveVotingSongWithTitle("노래1"); final VotingSong secondSong = saveVotingSongWithTitle("노래2"); final VotingSong thirdSong = saveVotingSongWithTitle("노래3"); 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 eba677d14..e3209daa7 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 @@ -11,14 +11,12 @@ import shook.shook.part.domain.PartLength; import shook.shook.part.exception.PartException; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.exception.VoteException; class VotingSongPartTest { private static Member MEMBER = new Member("a@a.com", "nickname"); - private final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + private final Artist artist = new Artist("profile", "가수"); private final VotingSong votingSong = new VotingSong( "제목", "비디오ID는 11글자", 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 05c5c4e8e..80e92d87b 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 @@ -8,8 +8,6 @@ import shook.shook.part.domain.PartLength; import shook.shook.part.exception.PartException; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; class VotingSongPartsTest { @@ -19,7 +17,7 @@ class VotingSongPartsTest { @Test void create_fail_duplicatePartExist() { //given - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( "제목", "비디오ID는 11글자", 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 593848366..aa6341bf5 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 @@ -7,13 +7,11 @@ import org.junit.jupiter.api.Test; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.exception.VotingSongPartException; class VotingSongTest { - private final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + private final Artist artist = new Artist("profile", "가수"); @DisplayName("파트 수집 중인 노래에 파트를 등록한다. ( 노래에 해당하는 파트일 때 )") @Test 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 536e6f799..43cdebb41 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 @@ -10,8 +10,6 @@ import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.Vote; @@ -41,7 +39,7 @@ class VoteRepositoryTest extends UsingJpaTest { void existsByMemberAndVotingSongPart() { //given final Member member = memberRepository.findById(1L).get(); - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); final VotingSong votingSong = votingSongRepository.save( new VotingSong( 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 aa49a7da8..68bae140e 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 @@ -13,8 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -35,7 +33,7 @@ class VotingSongPartRepositoryTest extends UsingJpaTest { @BeforeEach void setUp() { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); SAVED_SONG = votingSongRepository.save( new VotingSong( 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 9607d76a8..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 @@ -8,8 +8,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -23,7 +21,7 @@ class VotingSongRepositoryTest extends UsingJpaTest { private ArtistRepository artistRepository; private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( votingSongTitle, "12345678901", 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 2bd29a64b..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 @@ -11,8 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.AcceptanceTest; import shook.shook.voting_song.application.dto.VotingSongResponse; @@ -29,7 +27,7 @@ class VotingSongControllerTest extends AcceptanceTest { private ArtistRepository artistRepository; private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( votingSongTitle, "12345678901", 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 0e5963875..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 @@ -18,8 +18,6 @@ import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.voting_song.application.VotingSongPartService; import shook.shook.voting_song.application.dto.VotingSongPartRegisterRequest; @@ -96,7 +94,7 @@ private String getToken(final Long memberId, final String nickname) { } private VotingSong getSavedSong() { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); return votingSongRepository.save(new VotingSong( "title", From 8768c5fc7f22a1e070b26a4ac5e0692db694b085 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Wed, 18 Oct 2023 15:45:30 +0900 Subject: [PATCH 07/21] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=20=EA=B0=80?= =?UTF-8?q?=EC=88=98=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EC=8B=9C,=20=EB=85=B8=EB=9E=98=EB=B3=84=20=EA=B0=80=EC=88=98?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20response=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/ArtistWithSongSearchResponse.java | 7 ++++--- .../song/application/dto/SongSearchResponse.java | 14 +++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) 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 index a495dce4f..eaabf48c6 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java @@ -35,13 +35,14 @@ public static ArtistWithSongSearchResponse of(final Artist artist, final int tot artist.getArtistName(), artist.getProfileImageUrl(), totalSongCount, - convertToSongSearchResponse(songs) + convertToSongSearchResponse(songs, artist.getArtistName()) ); } - private static List convertToSongSearchResponse(final List songs) { + private static List convertToSongSearchResponse(final List songs, + final String singer) { return songs.stream() - .map(SongSearchResponse::from) + .map(song -> SongSearchResponse.from(song, singer)) .toList(); } } 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 index 7eb66a45a..bc32b2d4a 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java @@ -23,8 +23,16 @@ public class SongSearchResponse { @Schema(description = "노래 비디오 길이", example = "247") private final int videoLength; - public static SongSearchResponse from(final Song song) { - return new SongSearchResponse(song.getId(), song.getTitle(), song.getAlbumCoverUrl(), - song.getLength()); + @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 + ); } } From 86deb04e4fcb1a96e62914f49aa24de567e21b6e Mon Sep 17 00:00:00 2001 From: somsom13 Date: Wed, 18 Oct 2023 15:55:22 +0900 Subject: [PATCH 08/21] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D,=20=EA=B0=9C=ED=96=89=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/song/domain/repository/SongRepository.java | 3 --- .../domain/repository/VotingSongPartRepositoryTest.java | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) 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 bad8cd512..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 @@ -31,9 +31,6 @@ public interface SongRepository extends JpaRepository { + "HAVING SUM(COALESCE(kp.likeCount, 0)) < (SELECT SUM(COALESCE(kp2.likeCount, 0)) FROM KillingPart kp2 WHERE kp2.song.id = :id) " + "OR (SUM(COALESCE(kp.likeCount, 0)) = (SELECT SUM(COALESCE(kp3.likeCount, 0)) FROM KillingPart kp3 WHERE kp3.song.id = :id) AND s.id < :id) " + "ORDER BY SUM(COALESCE(kp.likeCount, 0)) DESC, s.id DESC") - // id 로 song 찾아온느 쿼리 1개 -> 비즈니스 로직에서 조건 필터링 => 2번을 1번으로 - // 100개 이하의 데이터는 비즈니스 로직에서 정렬하는 것을 추천한다. - // 조건이 확실하게 있는 경우는 쿼리에서 하는 것이 좋다. List findSongsWithLessLikeCountThanSongWithId( @Param("id") final Long songId, final Pageable pageable 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 68bae140e..e370363d6 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 @@ -49,8 +49,7 @@ void setUp() { @Test void save() { //given - final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, - SAVED_SONG); + final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, SAVED_SONG); //when final VotingSongPart saved = votingSongPartRepository.save(votingSongPart); @@ -64,8 +63,7 @@ void save() { @Test void createdAt() { //given - final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, - SAVED_SONG); + final VotingSongPart votingSongPart = VotingSongPart.forSave(14, PartLength.SHORT, SAVED_SONG); //when final LocalDateTime prev = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); @@ -88,8 +86,7 @@ void findAllBySong() { //when saveAndClearEntityManager(); - final List allBySong = votingSongPartRepository.findAllByVotingSong( - SAVED_SONG); + final List allBySong = votingSongPartRepository.findAllByVotingSong(SAVED_SONG); //then assertThat(allBySong).containsAll(List.of(firstPart, secondPart)); From cd164ad692b6c1e149e2b24e6fdc182833d52ed7 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Mon, 9 Oct 2023 23:00:16 +0900 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20Artist=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20Song=20singer=20->?= =?UTF-8?q?=20artist=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/globalexception/ErrorCode.java | 3 + .../GlobalExceptionHandler.java | 2 + .../song/application/SongDataExcelReader.java | 8 +- .../shook/song/application/SongService.java | 8 +- .../dto/LikedKillingPartResponse.java | 2 +- .../song/application/dto/SongResponse.java | 2 +- .../SongWithKillingPartsRegisterRequest.java | 20 ++- .../dto/HighLikedSongResponse.java | 2 +- .../java/shook/shook/song/domain/Artist.java | 70 ++++++++ .../shook/shook/song/domain/ArtistName.java | 39 +++++ .../shook/song/domain/ProfileImageUrl.java | 39 +++++ .../java/shook/shook/song/domain/Singer.java | 7 +- .../java/shook/shook/song/domain/Song.java | 25 +-- .../domain/repository/ArtistRepository.java | 8 + .../shook/song/exception/ArtistException.java | 63 +++++++ .../shook/song/exception/SongException.java | 22 --- .../application/VotingSongService.java | 6 +- .../dto/VotingSongRegisterRequest.java | 16 +- .../application/dto/VotingSongResponse.java | 2 +- .../shook/voting_song/domain/VotingSong.java | 19 ++- .../ControllerAdviceTest.java | 9 +- .../song/application/SongServiceTest.java | 23 ++- .../{SingerTest.java => ArtistNameTest.java} | 20 +-- .../shook/shook/song/domain/SongTest.java | 26 ++- .../domain/killingpart/KillingPartTest.java | 16 +- .../repository/KillingPartRepositoryTest.java | 21 ++- .../domain/repository/SongRepositoryTest.java | 148 ++++++++++------ .../song/ui/AdminSongControllerTest.java | 33 +--- .../song/ui/HighLikedSongControllerTest.java | 2 + .../shook/song/ui/MyPageControllerTest.java | 17 +- .../VotingSongPartServiceTest.java | 23 ++- .../application/VotingSongServiceTest.java | 81 +++++---- .../domain/VotingSongPartTest.java | 12 +- .../domain/VotingSongPartsTest.java | 12 +- .../voting_song/domain/VotingSongTest.java | 30 +++- .../domain/repository/VoteRepositoryTest.java | 25 ++- .../VotingSongPartRepositoryTest.java | 18 +- .../repository/VotingSongRepositoryTest.java | 161 ++++++------------ .../ui/VotingSongControllerTest.java | 48 ++++-- .../ui/VotingSongPartControllerTest.java | 17 +- .../initialize_killing_part_song.sql | 32 +++- backend/src/test/resources/schema-test.sql | 14 +- 42 files changed, 796 insertions(+), 355 deletions(-) create mode 100644 backend/src/main/java/shook/shook/song/domain/Artist.java create mode 100644 backend/src/main/java/shook/shook/song/domain/ArtistName.java create mode 100644 backend/src/main/java/shook/shook/song/domain/ProfileImageUrl.java create mode 100644 backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java create mode 100644 backend/src/main/java/shook/shook/song/exception/ArtistException.java rename backend/src/test/java/shook/shook/song/domain/{SingerTest.java => ArtistNameTest.java} (62%) diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 832db7b6e..885e7d2ea 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -54,6 +54,9 @@ 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자를 넘길 수 없습니다."), + // 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/SongDataExcelReader.java b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java index 67dec6805..a53f6aa89 100644 --- a/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java +++ b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java @@ -15,8 +15,11 @@ 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.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.exception.SongDataFileReadException; @@ -90,7 +93,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(new ProfileImageUrl("image"), new ArtistName("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/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/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/SongWithKillingPartsRegisterRequest.java b/backend/src/main/java/shook/shook/song/application/dto/SongWithKillingPartsRegisterRequest.java index 58494018e..109bc025c 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,8 +10,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; @Schema(description = "노래와 킬링파트 등록 요청") @@ -34,7 +37,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 +57,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(new ProfileImageUrl(profileImageUrl), new ArtistName(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..88d181e54 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -0,0 +1,70 @@ +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 ProfileImageUrl profileImageUrl, final ArtistName artistName) { + this.profileImageUrl = profileImageUrl; + this.artistName = artistName; + } + + public String getArtistName() { + return artistName.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..e6830ea2f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.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 ArtistName { + + private static final int NAME_MAXIMUM_LENGTH = 50; + + @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) + ); + } + } +} 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/repository/ArtistRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java new file mode 100644 index 000000000..e8c625a5e --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java @@ -0,0 +1,8 @@ +package shook.shook.song.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import shook.shook.song.domain.Artist; + +public interface ArtistRepository extends JpaRepository { + +} 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..4622ef3d3 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -0,0 +1,63 @@ +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); + } + } +} 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/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..4e4189dab 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,9 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import shook.shook.song.domain.Artist; +import shook.shook.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.domain.VotingSong; @Schema(description = "파트 수집 중인 노래 등록 요청") @@ -30,7 +33,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 +45,11 @@ public class VotingSongRegisterRequest { private Integer length; public VotingSong getVotingSong() { - return new VotingSong(title, videoId, imageUrl, singer, length); + final Artist artist = new Artist( + new ProfileImageUrl(profileImageUrl), + new ArtistName(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/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/song/application/SongServiceTest.java b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java index dd74f0835..2f5b34ebf 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) ); @@ -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), diff --git a/backend/src/test/java/shook/shook/song/domain/SingerTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java similarity index 62% rename from backend/src/test/java/shook/shook/song/domain/SingerTest.java rename to backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index 31fedd8b2..57bf851d5 100644 --- a/backend/src/test/java/shook/shook/song/domain/SingerTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -8,9 +8,9 @@ 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; +import shook.shook.song.exception.ArtistException; -class SingerTest { +class ArtistNameTest { @DisplayName("가수을 뜻하는 객체를 생성한다.") @Test @@ -18,7 +18,7 @@ void create_success() { //given //when //then - Assertions.assertDoesNotThrow(() -> new Singer("이름")); + Assertions.assertDoesNotThrow(() -> new ArtistName("이름")); } @DisplayName("가수 이름이 유효하지 않으면 예외를 던진다.") @@ -29,19 +29,19 @@ void create_fail_lessThanOne(final String name) { //given //when //then - assertThatThrownBy(() -> new Singer(name)) - .isInstanceOf(SongException.NullOrEmptySingerNameException.class); + assertThatThrownBy(() -> new ArtistName(name)) + .isInstanceOf(ArtistException.NullOrEmptyNameException.class); } - @DisplayName("가수 이름의 길이가 100을 넘을 경우 예외를 던진다.") + @DisplayName("가수 이름의 길이가 50을 넘을 경우 예외를 던진다.") @Test - void create_fail_lengthOver100() { + void create_fail_lengthOver50() { //given - final String name = ".".repeat(101); + final String name = ".".repeat(51); //when //then - assertThatThrownBy(() -> new Singer(name)) - .isInstanceOf(SongException.TooLongSingerNameException.class); + assertThatThrownBy(() -> new ArtistName(name)) + .isInstanceOf(ArtistException.TooLongNameException.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..87400b286 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(new ProfileImageUrl("image"), new ArtistName("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(new ProfileImageUrl("image"), new ArtistName("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/killingpart/KillingPartTest.java b/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartTest.java index f8d784127..a20c457a8 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,8 +8,11 @@ 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.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.exception.SongException; import shook.shook.song.exception.killingpart.KillingPartCommentException; @@ -140,10 +143,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(new ProfileImageUrl("image"), new ArtistName("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..f66f7d61d 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,14 @@ 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.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; 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 +31,9 @@ class KillingPartRepositoryTest extends UsingJpaTest { @Autowired private KillingPartRepository killingPartRepository; + @Autowired + private ArtistRepository artistRepository; + @Autowired private SongRepository songRepository; @@ -42,8 +49,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(new ProfileImageUrl("image"), new ArtistName("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 를 모두 저장한다.") 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..9edf81efd 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 @@ -12,8 +12,11 @@ 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.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.domain.killingpart.KillingPartLike; @@ -36,14 +39,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(new ProfileImageUrl("image"), new ArtistName("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 +71,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 @@ -79,8 +97,7 @@ void findById() { //then assertThat(findSong).isPresent(); - assertThat(findSong.get()).usingRecursiveComparison() - .isEqualTo(song); + assertThat(findSong.get()).isEqualTo(song); } @DisplayName("Song 을 저장할 때의 시간 정보로 createAt이 자동 생성된다.") @@ -91,7 +108,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 +122,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 +158,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 +210,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 +274,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 +282,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 +326,7 @@ void findSongsWithMoreLikeCountThanSongWithId() { // when saveAndClearEntityManager(); + final List songs = songRepository.findSongsWithMoreLikeCountThanSongWithId( standardSong.getId(), PageRequest.of(0, 10) @@ -303,9 +335,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 +354,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 +396,12 @@ 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)) + ); } } 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/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/voting_song/application/VotingSongPartServiceTest.java b/backend/src/test/java/shook/shook/voting_song/application/VotingSongPartServiceTest.java index c223cb647..8ee9198ca 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +46,9 @@ class VotingSongPartServiceTest extends UsingJpaTest { @Autowired private VoteRepository voteRepository; + @Autowired + private ArtistRepository artistRepository; + private VotingSongPartService votingSongPartService; @BeforeEach @@ -54,7 +61,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + artistRepository.save(artist); + SAVED_SONG = votingSongRepository.save(new VotingSong( + "노래제목", + "비디오ID는 11글자", + "이미지URL", + artist, + 180) + ); } void addPart(final VotingSong votingSong, final VotingSongPart votingSongPart) { @@ -102,8 +117,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 +141,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..763ddd1dd 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,11 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; 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 +26,46 @@ class VotingSongServiceTest extends UsingJpaTest { + public 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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +80,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 +93,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +133,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..7b61e585f 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,22 @@ 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.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; 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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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..03af0c099 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,9 @@ 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.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; class VotingSongPartsTest { @@ -16,7 +19,14 @@ class VotingSongPartsTest { @Test void create_fail_duplicatePartExist() { //given - final VotingSong votingSong = new VotingSong("제목", "비디오ID는 11글자", "이미지URL", "가수", 30); + final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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..3db6eb9b5 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,31 @@ 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.song.domain.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.exception.VotingSongPartException; class VotingSongTest { + private final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + @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 +40,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..f942f6c74 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +33,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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..aa3f070bd 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +27,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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..9607d76a8 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -15,6 +19,23 @@ class VotingSongRepositoryTest extends UsingJpaTest { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +44,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 +83,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 +118,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 +153,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..2bd29a64b 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +25,29 @@ class VotingSongControllerTest extends AcceptanceTest { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + + private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { + final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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 +74,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 +109,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 +139,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..0e5963875 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,10 @@ 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.ArtistName; +import shook.shook.song.domain.ProfileImageUrl; +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 +47,9 @@ void setUp() { @Autowired private VotingSongRepository votingSongRepository; + @Autowired + private ArtistRepository artistRepository; + @Autowired private VotingSongPartService votingSongPartService; @@ -89,7 +96,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(new ProfileImageUrl("profile"), new ArtistName("가수")); + 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..f38618b76 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -7,12 +7,13 @@ 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; 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 +61,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 +105,12 @@ create table if not exists member_part created_at timestamp(6) not null, primary key (id) ); + +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) +); From 84f436f9637dc9e3a5ef478a03666154a117a355 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 10 Oct 2023 13:32:45 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat:=20artist=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20song,=20voting=5Fs?= =?UTF-8?q?ong=20singer=20->=20artist=5Fid=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/dev/data.sql | 2 ++ backend/src/main/resources/dev/schema.sql | 14 ++++++++++++-- backend/src/main/resources/schema.sql | 14 ++++++++++++++ backend/src/test/resources/schema-test.sql | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index b24f7282b..b34ab0ce5 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -1,3 +1,5 @@ +-- 사용 불가한 data.sql + TRUNCATE TABLE song; TRUNCATE TABLE voting_song; diff --git a/backend/src/main/resources/dev/schema.sql b/backend/src/main/resources/dev/schema.sql index 6fc289aee..8e4dc8568 100644 --- a/backend/src/main/resources/dev/schema.sql +++ b/backend/src/main/resources/dev/schema.sql @@ -6,13 +6,14 @@ 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; 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 +61,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 +95,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, diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index ac451ebd6..d902e51ee 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -105,3 +105,17 @@ 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; diff --git a/backend/src/test/resources/schema-test.sql b/backend/src/test/resources/schema-test.sql index f38618b76..0610f97d8 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -108,7 +108,7 @@ create table if not exists member_part create table if not exists artist ( - id bigint generated by default as identity, + id bigint auto_increment, name varchar(50) not null, profile_image_url text not null, created_at timestamp(6) not null, From 5cbb92e20e7469364c61d19cbcd8bc089bc51b21 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 10 Oct 2023 17:00:14 +0900 Subject: [PATCH 11/21] =?UTF-8?q?feat:=20ArtistSynonym=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/globalexception/ErrorCode.java | 2 + .../shook/song/domain/ArtistSynonym.java | 62 +++++++++++++++++++ .../java/shook/shook/song/domain/Synonym.java | 39 ++++++++++++ .../shook/song/exception/ArtistException.java | 22 +++++++ backend/src/main/resources/dev/schema.sql | 9 +++ backend/src/main/resources/schema.sql | 8 +++ .../shook/song/domain/ArtistNameTest.java | 2 +- .../song/domain/ProfileImageUrlTest.java | 46 ++++++++++++++ .../shook/shook/song/domain/SynonymTest.java | 47 ++++++++++++++ backend/src/test/resources/schema-test.sql | 9 +++ 10 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java create mode 100644 backend/src/main/java/shook/shook/song/domain/Synonym.java create mode 100644 backend/src/test/java/shook/shook/song/domain/ProfileImageUrlTest.java create mode 100644 backend/src/test/java/shook/shook/song/domain/SynonymTest.java diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 885e7d2ea..a08c905ac 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -56,6 +56,8 @@ public enum ErrorCode { 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자를 넘길 수 없습니다.."), // 4000: 투표 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..1822f5b4b --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java @@ -0,0 +1,62 @@ +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.LAZY) + @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) + private Artist artist; + + @Embedded + private Synonym synonym; + + public String getArtistName() { + return artist.getArtistName(); + } + + public String getSynonym() { + return synonym.getValue(); + } + + @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/Synonym.java b/backend/src/main/java/shook/shook/song/domain/Synonym.java new file mode 100644 index 000000000..7d9a68ac1 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/Synonym.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 Synonym { + + private static final int MAXIMUM_LENGTH = 255; + + @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) + ); + } + } +} diff --git a/backend/src/main/java/shook/shook/song/exception/ArtistException.java b/backend/src/main/java/shook/shook/song/exception/ArtistException.java index 4622ef3d3..e1a0cbd34 100644 --- a/backend/src/main/java/shook/shook/song/exception/ArtistException.java +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -60,4 +60,26 @@ 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); + } + } } diff --git a/backend/src/main/resources/dev/schema.sql b/backend/src/main/resources/dev/schema.sql index 8e4dc8568..9cc59c431 100644 --- a/backend/src/main/resources/dev/schema.sql +++ b/backend/src/main/resources/dev/schema.sql @@ -8,6 +8,7 @@ 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 ( @@ -114,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 d902e51ee..fd75458fe 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -119,3 +119,11 @@ 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/song/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index 57bf851d5..b0770c3cc 100644 --- a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -12,7 +12,7 @@ class ArtistNameTest { - @DisplayName("가수을 뜻하는 객체를 생성한다.") + @DisplayName("가수 이름을 뜻하는 객체를 생성한다.") @Test void create_success() { //given 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/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java new file mode 100644 index 000000000..ae8eb7d21 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -0,0 +1,47 @@ +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.ArtistException; + +class SynonymTest { + + @DisplayName("가수 이름 동의어를 뜻하는 객체를 생성한다.") + @Test + void create_success() { + //given + //when + //then + Assertions.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); + } +} diff --git a/backend/src/test/resources/schema-test.sql b/backend/src/test/resources/schema-test.sql index 0610f97d8..d3598dd75 100644 --- a/backend/src/test/resources/schema-test.sql +++ b/backend/src/test/resources/schema-test.sql @@ -8,6 +8,7 @@ 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 ( @@ -114,3 +115,11 @@ create table if not exists artist 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) +); From 97891f8f9c3d03261b91d76d52e0b438bd3e5aad Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 12 Oct 2023 13:47:23 +0900 Subject: [PATCH 12/21] =?UTF-8?q?feat:=20=EA=B0=80=EC=88=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84,=20=EB=8F=99=EC=9D=98=EC=96=B4=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/globalexception/ErrorCode.java | 3 +- .../song/application/ArtistSearchService.java | 119 +++++++++ .../song/application/dto/ArtistResponse.java | 30 +++ .../dto/ArtistWithSongSearchResponse.java | 47 ++++ .../application/dto/SongSearchResponse.java | 30 +++ .../java/shook/shook/song/domain/Artist.java | 12 + .../shook/shook/song/domain/ArtistName.java | 29 +++ .../shook/song/domain/ArtistSynonym.java | 19 +- .../song/domain/InMemoryArtistSynonyms.java | 72 ++++++ .../InMemoryArtistSynonymsGenerator.java | 32 +++ .../java/shook/shook/song/domain/Synonym.java | 29 +++ .../domain/repository/ArtistRepository.java | 2 + .../repository/ArtistSynonymRepository.java | 10 + .../domain/repository/SongRepository.java | 9 + .../shook/song/exception/ArtistException.java | 11 + .../song/ui/ArtistSongSearchController.java | 44 ++++ .../song/ui/openapi/ArtistSongSearchApi.java | 78 ++++++ .../application/ArtistSearchServiceTest.java | 238 ++++++++++++++++++ .../shook/song/domain/ArtistNameTest.java | 47 ++++ .../domain/InMemoryArtistSynonymsTest.java | 164 ++++++++++++ .../shook/shook/song/domain/SynonymTest.java | 47 ++++ .../domain/repository/SongRepositoryTest.java | 36 +++ .../ui/ArtistSongSearchControllerTest.java | 153 +++++++++++ .../song/ui/SongSwipeControllerTest.java | 1 + 24 files changed, 1256 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/shook/shook/song/application/ArtistSearchService.java create mode 100644 backend/src/main/java/shook/shook/song/application/dto/ArtistResponse.java create mode 100644 backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java create mode 100644 backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java create mode 100644 backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonyms.java create mode 100644 backend/src/main/java/shook/shook/song/domain/InMemoryArtistSynonymsGenerator.java create mode 100644 backend/src/main/java/shook/shook/song/domain/repository/ArtistSynonymRepository.java create mode 100644 backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java create mode 100644 backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java create mode 100644 backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java create mode 100644 backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java create mode 100644 backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index a08c905ac..5afc0e13a 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -57,7 +57,8 @@ public enum ErrorCode { EMPTY_ARTIST_PROFILE_URL(3014, "가수 프로필 이미지는 비어있을 수 없습니다."), TOO_LONG_ARTIST_PROFILE_URL(3015, "가수 프로필 이미지URL은 65,356자를 넘길 수 없습니다."), EMPTY_ARTIST_SYNONYM(3016, "가수 동의어는 비어있을 수 없습니다."), - TOO_LONG_ARTIST_SYNONYM(3017, "가수 동의어는 255자를 넘길 수 없습니다.."), + TOO_LONG_ARTIST_SYNONYM(3017, "가수 동의어는 255자를 넘길 수 없습니다."), + ARTIST_NOT_EXIST(3018, "존재하지 않는 가수입니다."), // 4000: 투표 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..be249fc04 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java @@ -0,0 +1,119 @@ +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 lombok.extern.slf4j.Slf4j; +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.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 +@Slf4j +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; + + 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 all = songRepository.findAll(); + log.info("all song: {}", all); + final List songsWithTotalLikeCount = songRepository.findAllSongsWithTotalLikeCountByArtist( + artist); + + log.info("found song of artist: {}", songsWithTotalLikeCount); + 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)) + )); + } +} 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..a495dce4f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java @@ -0,0 +1,47 @@ +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) + ); + } + + private static List convertToSongSearchResponse(final List songs) { + return songs.stream() + .map(SongSearchResponse::from) + .toList(); + } +} 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..7eb66a45a --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.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.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; + + public static SongSearchResponse from(final Song song) { + return new SongSearchResponse(song.getId(), song.getTitle(), song.getAlbumCoverUrl(), + song.getLength()); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/Artist.java b/backend/src/main/java/shook/shook/song/domain/Artist.java index 88d181e54..7e7412247 100644 --- a/backend/src/main/java/shook/shook/song/domain/Artist.java +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -44,10 +44,22 @@ public Artist(final ProfileImageUrl profileImageUrl, final ArtistName artistName this.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) { diff --git a/backend/src/main/java/shook/shook/song/domain/ArtistName.java b/backend/src/main/java/shook/shook/song/domain/ArtistName.java index e6830ea2f..d3893a7e1 100644 --- a/backend/src/main/java/shook/shook/song/domain/ArtistName.java +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.java @@ -17,6 +17,7 @@ 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; @@ -36,4 +37,32 @@ private void validateName(final String value) { ); } } + + public boolean startsWithIgnoringCaseAndWhiteSpace(final String keyword) { + final String targetKeyword = toLowerCaseRemovingWhiteSpace(keyword); + if (StringChecker.isNullOrBlank(targetKeyword)) { + return false; + } + + return toLowerCaseRemovingWhiteSpace(value) + .startsWith(toLowerCaseRemovingWhiteSpace(keyword)); + } + + 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 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/ArtistSynonym.java b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java index 1822f5b4b..6fb63223f 100644 --- a/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java +++ b/backend/src/main/java/shook/shook/song/domain/ArtistSynonym.java @@ -25,19 +25,28 @@ public class ArtistSynonym { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "artist_id", foreignKey = @ForeignKey(name = "none"), updatable = false, nullable = false) private Artist artist; @Embedded private Synonym synonym; - public String getArtistName() { - return artist.getArtistName(); + 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 String getSynonym() { - return synonym.getValue(); + public boolean endsWith(final String keyword) { + return synonym.endsWithIgnoringCaseAndWhiteSpace(keyword); + } + + public String getArtistName() { + return artist.getArtistName(); } @Override 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/Synonym.java b/backend/src/main/java/shook/shook/song/domain/Synonym.java index 7d9a68ac1..450e197a3 100644 --- a/backend/src/main/java/shook/shook/song/domain/Synonym.java +++ b/backend/src/main/java/shook/shook/song/domain/Synonym.java @@ -17,6 +17,7 @@ public class Synonym { private static final int MAXIMUM_LENGTH = 255; + private static final String BLANK = "\\s"; @Column(name = "synonym", nullable = false) private String value; @@ -36,4 +37,32 @@ private void validate(final String 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 index e8c625a5e..ded9e3822 100644 --- a/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java +++ b/backend/src/main/java/shook/shook/song/domain/repository/ArtistRepository.java @@ -1,8 +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 index e1a0cbd34..ee73f0188 100644 --- a/backend/src/main/java/shook/shook/song/exception/ArtistException.java +++ b/backend/src/main/java/shook/shook/song/exception/ArtistException.java @@ -82,4 +82,15 @@ 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/ui/ArtistSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java new file mode 100644 index 000000000..d7128995f --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java @@ -0,0 +1,44 @@ +package shook.shook.song.ui; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.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.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.ui.openapi.ArtistSongSearchApi; + +@RequiredArgsConstructor +@RequestMapping("/singers") +@RestController +@Slf4j +public class ArtistSongSearchController implements ArtistSongSearchApi { + + private final ArtistSearchService artistSearchService; + + @GetMapping(params = {"name", "search=singer,song"}) + public ResponseEntity> searchArtistWithSongByKeyword( + @RequestParam(name = "search") final List searchTypes, + @RequestParam(name = "name") final String name) { + return ResponseEntity.ok(artistSearchService.searchArtistsAndTopSongsByKeyword(name)); + } + + @GetMapping(params = {"name", "search=singer"}) + public ResponseEntity> searchArtistByKeyword( + @RequestParam(name = "search") final String search, + @RequestParam(name = "name") final String name) { + return ResponseEntity.ok(artistSearchService.searchArtistsByKeyword(name)); + } + + @GetMapping("/{artist_id}") + public ResponseEntity searchSongsByArtist( + @PathVariable(name = "artist_id") final Long artistId) { + return ResponseEntity.ok(artistSearchService.searchAllSongsByArtist(artistId)); + } +} diff --git a/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java b/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java new file mode 100644 index 000000000..23be2bdee --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java @@ -0,0 +1,78 @@ +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.media.Schema; +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.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import shook.shook.song.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; + +@Tag(name = "Artist Search", description = "가수 이름 검색 API") +public interface ArtistSongSearchApi { + + @Operation( + summary = "검색어 입력 시 자동 완성되는 가수의 정보 검색", + description = "검색어 입력 시, 검색어로 시작하는 가수의 정보를 검색한다." + ) + @ApiResponse( + responseCode = "200", + description = "가수 검색 성공" + ) + @GetMapping(value = "?search=artist", params = {"name"}) + ResponseEntity> searchArtistByKeyword( + @Parameter(name = "search", description = "검색 타입", + schema = @Schema(enumAsRef = true, allowableValues = {"artist"})) + @RequestParam(name = "search") final String search, + @Parameter( + name = "name", + description = "검색할 가수 키워드", + required = true + ) + @RequestParam(name = "name") final String name + ); + + @Operation( + summary = "검색 시, 검색 결과 조회", + description = "검색 시, 검색어로 시작하거나 끝나는 가수와 해당 가수의 TOP3 노래가 조회된다." + ) + @ApiResponse( + responseCode = "200", + description = "가수, TOP3 노래 검색 성공" + ) + @GetMapping(value = "?search=artist,song", params = {"name"}) + ResponseEntity> searchArtistWithSongByKeyword( + @Parameter(name = "search", description = "검색 타입", + schema = @Schema(enumAsRef = true, allowableValues = {"artist", "song"})) + @RequestParam(name = "search") final List searchTypes, + @Parameter( + name = "name", + description = "검색할 가수 키워드", + required = true + ) + @RequestParam(name = "name") final String name + ); + + @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/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..36230cff2 --- /dev/null +++ b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java @@ -0,0 +1,238 @@ +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.part.domain.PartLength; +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.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.SongRepository; +import shook.shook.song.exception.ArtistException; +import shook.shook.support.UsingJpaTest; + +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +class ArtistSearchServiceTest extends UsingJpaTest { + + private ArtistSearchService artistSearchService; + private InMemoryArtistSynonyms artistSynonyms = new InMemoryArtistSynonyms(); + + @Autowired + private SongRepository songRepository; + + @Autowired + private ArtistRepository artistRepository; + + + @Autowired + private KillingPartLikeRepository likeRepository; + + @Autowired + private KillingPartRepository killingPartRepository; + + @Autowired + private MemberRepository memberRepository; + + @BeforeEach + void setUp() { + artistSearchService = new ArtistSearchService(artistSynonyms, artistRepository, + songRepository); + 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, PartLength.SHORT); + final KillingPart secondKillingPart = KillingPart.forSave(15, PartLength.SHORT); + final KillingPart thirdKillingPart = KillingPart.forSave(20, PartLength.SHORT); + + 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/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index b0770c3cc..5bb90c9f4 100644 --- a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -1,11 +1,13 @@ package shook.shook.song.domain; +import static org.assertj.core.api.Assertions.assertThat; 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.CsvSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import shook.shook.song.exception.ArtistException; @@ -44,4 +46,49 @@ void create_fail_lengthOver50() { 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..f048698ac --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java @@ -0,0 +1,164 @@ +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(new ProfileImageUrl("image"), new ArtistName("name1")); + artist2 = new Artist(new ProfileImageUrl("image"), new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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(new ProfileImageUrl("image"), + new ArtistName("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/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java index ae8eb7d21..5e06baf9d 100644 --- a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -1,11 +1,13 @@ package shook.shook.song.domain; +import static org.assertj.core.api.Assertions.assertThat; 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.CsvSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import shook.shook.song.exception.ArtistException; @@ -44,4 +46,49 @@ void create_fail_lengthOver255() { 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/repository/SongRepositoryTest.java b/backend/src/test/java/shook/shook/song/domain/repository/SongRepositoryTest.java index 9edf81efd..c925969e1 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; @@ -404,4 +405,39 @@ void findSongsWithMoreLikeCountThanSongWithId_smallData() { 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/ArtistSongSearchControllerTest.java b/backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java new file mode 100644 index 000000000..0166163eb --- /dev/null +++ b/backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java @@ -0,0 +1,153 @@ +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; +import shook.shook.song.domain.repository.ArtistSynonymRepository; + +@SuppressWarnings("NonAsciiCharacters") +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ArtistSongSearchControllerTest { + + 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 ArtistSynonymRepository synonymRepository; + + @Autowired + private ArtistRepository artistRepository; + + @DisplayName("search=singer,song name=검색어 으로 요청을 보내는 경우 상태코드 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("name", keyword, "search", searchType)) + .when().log().all() + .get("/singers") + .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("search=singer name=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하는 가수 목록을 반환한다.") + @Test + void searchArtistByKeyword() { + // given + final String searchType = "singer"; + final String keyword = "인기"; + + // when + final List response = RestAssured.given().log().all() + .params(Map.of("name", keyword, "search", searchType)) + .when().log().all() + .get("/singers") + .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()) + ); + } + + @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); + } +} 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 { From 92b40da017a2189d5901814f2e8e3936a5223aa9 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Tue, 17 Oct 2023 14:34:35 +0900 Subject: [PATCH 13/21] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../song/application/ArtistSearchService.java | 5 ----- .../song/application/SongDataExcelReader.java | 4 +--- .../SongWithKillingPartsRegisterRequest.java | 4 +--- .../java/shook/shook/song/domain/Artist.java | 6 +++--- .../shook/shook/song/domain/ArtistName.java | 8 ++++---- .../song/domain/repository/SongRepository.java | 3 +++ .../song/ui/ArtistSongSearchController.java | 2 -- .../dto/VotingSongRegisterRequest.java | 9 ++------- .../shook/shook/song/domain/ArtistNameTest.java | 4 ++-- .../song/domain/InMemoryArtistSynonymsTest.java | 16 ++++++---------- .../java/shook/shook/song/domain/SongTest.java | 4 ++-- .../shook/shook/song/domain/SynonymTest.java | 4 ++-- .../domain/killingpart/KillingPartTest.java | 4 +--- .../repository/KillingPartRepositoryTest.java | 17 +++++++---------- .../domain/repository/SongRepositoryTest.java | 8 +++----- .../application/VotingSongPartServiceTest.java | 4 +--- .../application/VotingSongServiceTest.java | 9 ++++----- .../voting_song/domain/VotingSongPartTest.java | 4 +--- .../voting_song/domain/VotingSongPartsTest.java | 4 +--- .../voting_song/domain/VotingSongTest.java | 4 +--- .../domain/repository/VoteRepositoryTest.java | 4 +--- .../VotingSongPartRepositoryTest.java | 4 +--- .../repository/VotingSongRepositoryTest.java | 4 +--- .../ui/VotingSongControllerTest.java | 4 +--- .../ui/VotingSongPartControllerTest.java | 4 +--- 25 files changed, 50 insertions(+), 93 deletions(-) diff --git a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java index be249fc04..c38bb94cf 100644 --- a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java +++ b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shook.shook.song.application.dto.ArtistResponse; @@ -21,7 +20,6 @@ @RequiredArgsConstructor @Transactional(readOnly = true) @Service -@Slf4j public class ArtistSearchService { private static final int TOP_SONG_COUNT_OF_ARTIST = 3; @@ -87,12 +85,9 @@ private List getTopSongsOfArtist(final Artist artist) { } private List getSongsOfArtistSortedByLikeCount(final Artist artist) { - final List all = songRepository.findAll(); - log.info("all song: {}", all); final List songsWithTotalLikeCount = songRepository.findAllSongsWithTotalLikeCountByArtist( artist); - log.info("found song of artist: {}", songsWithTotalLikeCount); return songsWithTotalLikeCount.stream() .sorted(Comparator.comparing(SongTotalLikeCountDto::getTotalLikeCount, Comparator.reverseOrder()) 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 a53f6aa89..4e71ed352 100644 --- a/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java +++ b/backend/src/main/java/shook/shook/song/application/SongDataExcelReader.java @@ -16,10 +16,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.exception.SongDataFileReadException; @@ -94,7 +92,7 @@ private Optional parseToSong(final Row currentRow) { return killingParts.map( parts -> new Song(title, videoId, albumCoverUrl, - new Artist(new ProfileImageUrl("image"), new ArtistName("name")), length, + new Artist("image", "name"), length, Genre.from(genre), parts)); } 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 109bc025c..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 @@ -11,10 +11,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; @Schema(description = "노래와 킬링파트 등록 요청") @@ -61,7 +59,7 @@ public Song convertToSong() { title, videoId, imageUrl, - new Artist(new ProfileImageUrl(profileImageUrl), new ArtistName(artistName)), + new Artist(profileImageUrl, artistName), length, Genre.from(genre), convertToKillingParts() diff --git a/backend/src/main/java/shook/shook/song/domain/Artist.java b/backend/src/main/java/shook/shook/song/domain/Artist.java index 7e7412247..67eada87a 100644 --- a/backend/src/main/java/shook/shook/song/domain/Artist.java +++ b/backend/src/main/java/shook/shook/song/domain/Artist.java @@ -39,9 +39,9 @@ private void prePersist() { createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); } - public Artist(final ProfileImageUrl profileImageUrl, final ArtistName artistName) { - this.profileImageUrl = profileImageUrl; - this.artistName = artistName; + public Artist(final String profileImageUrl, final String artistName) { + this.profileImageUrl = new ProfileImageUrl(profileImageUrl); + this.artistName = new ArtistName(artistName); } public boolean nameStartsWith(final String keyword) { diff --git a/backend/src/main/java/shook/shook/song/domain/ArtistName.java b/backend/src/main/java/shook/shook/song/domain/ArtistName.java index d3893a7e1..a3198e596 100644 --- a/backend/src/main/java/shook/shook/song/domain/ArtistName.java +++ b/backend/src/main/java/shook/shook/song/domain/ArtistName.java @@ -48,6 +48,10 @@ public boolean startsWithIgnoringCaseAndWhiteSpace(final String keyword) { .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)) { @@ -58,10 +62,6 @@ public boolean endsWithIgnoringCaseAndWhiteSpace(final String keyword) { .endsWith(toLowerCaseRemovingWhiteSpace(keyword)); } - 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/SongRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java index a0c09ce1f..bad8cd512 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 @@ -31,6 +31,9 @@ public interface SongRepository extends JpaRepository { + "HAVING SUM(COALESCE(kp.likeCount, 0)) < (SELECT SUM(COALESCE(kp2.likeCount, 0)) FROM KillingPart kp2 WHERE kp2.song.id = :id) " + "OR (SUM(COALESCE(kp.likeCount, 0)) = (SELECT SUM(COALESCE(kp3.likeCount, 0)) FROM KillingPart kp3 WHERE kp3.song.id = :id) AND s.id < :id) " + "ORDER BY SUM(COALESCE(kp.likeCount, 0)) DESC, s.id DESC") + // id 로 song 찾아온느 쿼리 1개 -> 비즈니스 로직에서 조건 필터링 => 2번을 1번으로 + // 100개 이하의 데이터는 비즈니스 로직에서 정렬하는 것을 추천한다. + // 조건이 확실하게 있는 경우는 쿼리에서 하는 것이 좋다. List findSongsWithLessLikeCountThanSongWithId( @Param("id") final Long songId, final Pageable pageable diff --git a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java index d7128995f..16cfc36b2 100644 --- a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java +++ b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java @@ -2,7 +2,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,7 +16,6 @@ @RequiredArgsConstructor @RequestMapping("/singers") @RestController -@Slf4j public class ArtistSongSearchController implements ArtistSongSearchApi { private final ArtistSearchService artistSearchService; 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 4e4189dab..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 @@ -9,8 +9,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.domain.VotingSong; @Schema(description = "파트 수집 중인 노래 등록 요청") @@ -45,11 +43,8 @@ public class VotingSongRegisterRequest { private Integer length; public VotingSong getVotingSong() { - final Artist artist = new Artist( - new ProfileImageUrl(profileImageUrl), - new ArtistName(artistName) - ); - + final Artist artist = new Artist(profileImageUrl, artistName); + return new VotingSong(title, videoId, imageUrl, artist, length); } } diff --git a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java index 5bb90c9f4..ffb551f61 100644 --- a/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java +++ b/backend/src/test/java/shook/shook/song/domain/ArtistNameTest.java @@ -2,8 +2,8 @@ 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.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -20,7 +20,7 @@ void create_success() { //given //when //then - Assertions.assertDoesNotThrow(() -> new ArtistName("이름")); + assertDoesNotThrow(() -> new ArtistName("이름")); } @DisplayName("가수 이름이 유효하지 않으면 예외를 던진다.") diff --git a/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java index f048698ac..f5b598d5c 100644 --- a/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java +++ b/backend/src/test/java/shook/shook/song/domain/InMemoryArtistSynonymsTest.java @@ -36,8 +36,8 @@ class InMemoryArtistSynonymsTest { @BeforeEach void setUp() { - artist1 = new Artist(new ProfileImageUrl("image"), new ArtistName("name1")); - artist2 = new Artist(new ProfileImageUrl("image"), new ArtistName("name2")); + artist1 = new Artist("image", "name1"); + artist2 = new Artist("image", "name2"); artistRepository.saveAll(List.of(artist1, artist2)); synonym1 = new ArtistSynonym(artist1, new Synonym("synonym1")); @@ -62,8 +62,7 @@ void generator_initialize() { @Test void findAllArtistsHavingSynonymStartsOrEndsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newTestSy")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); @@ -82,8 +81,7 @@ void findAllArtistsHavingSynonymStartsOrEndsWith() { @Test void findAllArtistsHavingSynonymStartsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newTestSy")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); @@ -114,8 +112,7 @@ void findAllArtistsHavingSynonymStartsOrEndsWith_emptyInput() { @Test void findAllArtistsNameStartsOrEndsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newSynonym")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); @@ -134,8 +131,7 @@ void findAllArtistsNameStartsOrEndsWith() { @Test void findAllArtistsNameStartsWith() { // given - final Artist newArtist = new Artist(new ProfileImageUrl("image"), - new ArtistName("newName")); + final Artist newArtist = new Artist("image", "newName"); final ArtistSynonym newSynonym = new ArtistSynonym(newArtist, new Synonym("newSynonym")); artistRepository.save(newArtist); artistSynonymRepository.save(newSynonym); 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 87400b286..f3eb5c2b0 100644 --- a/backend/src/test/java/shook/shook/song/domain/SongTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SongTest.java @@ -21,7 +21,7 @@ void songCreate_nullKillingParts_fail() { "title", "videoId", "imageUrl", - new Artist(new ProfileImageUrl("image"), new ArtistName("name")), + new Artist("image", "name"), 300, Genre.from("댄스"), null @@ -40,7 +40,7 @@ void getPartVideoUrl() { List.of(killingPart1, killingPart2, killingPart3) ); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); final Song song = new Song( "title", "3rUPND6FG8A", diff --git a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java index 5e06baf9d..dee658b31 100644 --- a/backend/src/test/java/shook/shook/song/domain/SynonymTest.java +++ b/backend/src/test/java/shook/shook/song/domain/SynonymTest.java @@ -2,8 +2,8 @@ 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.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -20,7 +20,7 @@ void create_success() { //given //when //then - Assertions.assertDoesNotThrow(() -> new Synonym("동의어")); + assertDoesNotThrow(() -> new Synonym("동의어")); } @DisplayName("가수 이름 동의어가 유효하지 않으면 예외를 던진다.") 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 a20c457a8..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 @@ -9,10 +9,8 @@ import org.junit.jupiter.api.Test; import shook.shook.member.domain.Member; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.exception.SongException; import shook.shook.song.exception.killingpart.KillingPartCommentException; @@ -143,7 +141,7 @@ 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 Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); final Song song = new Song( "title", "3rUPND6FG8A", 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 f66f7d61d..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 @@ -10,10 +10,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.domain.repository.ArtistRepository; @@ -49,7 +47,7 @@ void setUp() { THIRD_KILLING_PART ) ); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); final Song song = new Song( "title", "3rUPND6FG8A", @@ -72,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 c925969e1..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 @@ -14,10 +14,8 @@ import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; import shook.shook.song.domain.Genre; import shook.shook.song.domain.KillingParts; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.Song; import shook.shook.song.domain.killingpart.KillingPart; import shook.shook.song.domain.killingpart.KillingPartLike; @@ -48,7 +46,7 @@ private Song createNewSongWithKillingParts() { final KillingPart secondKillingPart = KillingPart.forSave(15, 5); final KillingPart thirdKillingPart = KillingPart.forSave(20, 5); - final Artist artist = new Artist(new ProfileImageUrl("image"), new ArtistName("name")); + final Artist artist = new Artist("image", "name"); return new Song( "title", "3rUPND6FG8A", @@ -97,8 +95,8 @@ void findById() { final Optional findSong = songRepository.findById(song.getId()); //then - assertThat(findSong).isPresent(); - assertThat(findSong.get()).isEqualTo(song); + assertThat(findSong).isPresent() + .get().isEqualTo(song); } @DisplayName("Song 을 저장할 때의 시간 정보로 createAt이 자동 생성된다.") 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 8ee9198ca..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 @@ -15,8 +15,6 @@ import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.application.dto.VotingSongPartRegisterRequest; @@ -61,7 +59,7 @@ void setUp() { ); FIRST_MEMBER = memberRepository.save(new Member("a@a.com", "nickname")); SECOND_MEMBER = memberRepository.save(new Member("b@b.com", "nickname")); - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); SAVED_SONG = votingSongRepository.save(new VotingSong( "노래제목", 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 763ddd1dd..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 @@ -12,8 +12,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.SongTitle; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; @@ -26,7 +24,8 @@ class VotingSongServiceTest extends UsingJpaTest { - public static final String VIDEO_ID = "비디오ID는 11글자"; + private static final String VIDEO_ID = "비디오ID는 11글자"; + @Autowired private VotingSongRepository votingSongRepository; @@ -41,7 +40,7 @@ void setUp() { } private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( votingSongTitle, VIDEO_ID, @@ -93,7 +92,7 @@ class findAll { @Test void findAllVotingSongs() { // given - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong firstSong = saveVotingSongWithTitle("노래1"); final VotingSong secondSong = saveVotingSongWithTitle("노래2"); final VotingSong thirdSong = saveVotingSongWithTitle("노래3"); 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 7b61e585f..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 @@ -11,14 +11,12 @@ import shook.shook.part.domain.PartLength; import shook.shook.part.exception.PartException; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.exception.VoteException; class VotingSongPartTest { private static Member MEMBER = new Member("a@a.com", "nickname"); - private final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + private final Artist artist = new Artist("profile", "가수"); private final VotingSong votingSong = new VotingSong( "제목", "비디오ID는 11글자", 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 03af0c099..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 @@ -8,8 +8,6 @@ import shook.shook.part.domain.PartLength; import shook.shook.part.exception.PartException; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; class VotingSongPartsTest { @@ -19,7 +17,7 @@ class VotingSongPartsTest { @Test void create_fail_duplicatePartExist() { //given - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( "제목", "비디오ID는 11글자", 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 3db6eb9b5..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 @@ -7,13 +7,11 @@ import org.junit.jupiter.api.Test; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.voting_song.exception.VotingSongPartException; class VotingSongTest { - private final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + private final Artist artist = new Artist("profile", "가수"); @DisplayName("파트 수집 중인 노래에 파트를 등록한다. ( 노래에 해당하는 파트일 때 )") @Test 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 f942f6c74..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 @@ -10,8 +10,6 @@ import shook.shook.member.domain.repository.MemberRepository; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.Vote; @@ -41,7 +39,7 @@ class VoteRepositoryTest extends UsingJpaTest { void existsByMemberAndVotingSongPart() { //given final Member member = memberRepository.findById(1L).get(); - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); final VotingSong votingSong = votingSongRepository.save( new VotingSong( 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 aa3f070bd..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 @@ -13,8 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import shook.shook.part.domain.PartLength; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -35,7 +33,7 @@ class VotingSongPartRepositoryTest extends UsingJpaTest { @BeforeEach void setUp() { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); SAVED_SONG = votingSongRepository.save( new VotingSong( 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 9607d76a8..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 @@ -8,8 +8,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.UsingJpaTest; import shook.shook.voting_song.domain.VotingSong; @@ -23,7 +21,7 @@ class VotingSongRepositoryTest extends UsingJpaTest { private ArtistRepository artistRepository; private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( votingSongTitle, "12345678901", 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 2bd29a64b..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 @@ -11,8 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.support.AcceptanceTest; import shook.shook.voting_song.application.dto.VotingSongResponse; @@ -29,7 +27,7 @@ class VotingSongControllerTest extends AcceptanceTest { private ArtistRepository artistRepository; private VotingSong saveVotingSongWithTitle(final String votingSongTitle) { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); final VotingSong votingSong = new VotingSong( votingSongTitle, "12345678901", 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 0e5963875..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 @@ -18,8 +18,6 @@ import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.song.domain.Artist; -import shook.shook.song.domain.ArtistName; -import shook.shook.song.domain.ProfileImageUrl; import shook.shook.song.domain.repository.ArtistRepository; import shook.shook.voting_song.application.VotingSongPartService; import shook.shook.voting_song.application.dto.VotingSongPartRegisterRequest; @@ -96,7 +94,7 @@ private String getToken(final Long memberId, final String nickname) { } private VotingSong getSavedSong() { - final Artist artist = new Artist(new ProfileImageUrl("profile"), new ArtistName("가수")); + final Artist artist = new Artist("profile", "가수"); artistRepository.save(artist); return votingSongRepository.save(new VotingSong( "title", From 054e5ad3587336a8b5d9837961b6ebd03e402991 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Wed, 18 Oct 2023 15:45:30 +0900 Subject: [PATCH 14/21] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=20=EA=B0=80?= =?UTF-8?q?=EC=88=98=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EC=8B=9C,=20=EB=85=B8=EB=9E=98=EB=B3=84=20=EA=B0=80=EC=88=98?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20response=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/ArtistWithSongSearchResponse.java | 7 ++++--- .../song/application/dto/SongSearchResponse.java | 14 +++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) 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 index a495dce4f..eaabf48c6 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/ArtistWithSongSearchResponse.java @@ -35,13 +35,14 @@ public static ArtistWithSongSearchResponse of(final Artist artist, final int tot artist.getArtistName(), artist.getProfileImageUrl(), totalSongCount, - convertToSongSearchResponse(songs) + convertToSongSearchResponse(songs, artist.getArtistName()) ); } - private static List convertToSongSearchResponse(final List songs) { + private static List convertToSongSearchResponse(final List songs, + final String singer) { return songs.stream() - .map(SongSearchResponse::from) + .map(song -> SongSearchResponse.from(song, singer)) .toList(); } } 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 index 7eb66a45a..bc32b2d4a 100644 --- a/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java +++ b/backend/src/main/java/shook/shook/song/application/dto/SongSearchResponse.java @@ -23,8 +23,16 @@ public class SongSearchResponse { @Schema(description = "노래 비디오 길이", example = "247") private final int videoLength; - public static SongSearchResponse from(final Song song) { - return new SongSearchResponse(song.getId(), song.getTitle(), song.getAlbumCoverUrl(), - song.getLength()); + @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 + ); } } From 1c475b5a2b2fc57d52e621f2c623f4d19c9aa036 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Wed, 18 Oct 2023 15:55:22 +0900 Subject: [PATCH 15/21] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D,=20=EA=B0=9C=ED=96=89=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/shook/song/domain/repository/SongRepository.java | 3 --- 1 file changed, 3 deletions(-) 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 bad8cd512..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 @@ -31,9 +31,6 @@ public interface SongRepository extends JpaRepository { + "HAVING SUM(COALESCE(kp.likeCount, 0)) < (SELECT SUM(COALESCE(kp2.likeCount, 0)) FROM KillingPart kp2 WHERE kp2.song.id = :id) " + "OR (SUM(COALESCE(kp.likeCount, 0)) = (SELECT SUM(COALESCE(kp3.likeCount, 0)) FROM KillingPart kp3 WHERE kp3.song.id = :id) AND s.id < :id) " + "ORDER BY SUM(COALESCE(kp.likeCount, 0)) DESC, s.id DESC") - // id 로 song 찾아온느 쿼리 1개 -> 비즈니스 로직에서 조건 필터링 => 2번을 1번으로 - // 100개 이하의 데이터는 비즈니스 로직에서 정렬하는 것을 추천한다. - // 조건이 확실하게 있는 경우는 쿼리에서 하는 것이 좋다. List findSongsWithLessLikeCountThanSongWithId( @Param("id") final Long songId, final Pageable pageable From 8d98dad2ce4d475637cd29e82e9f61129fa921a5 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 19 Oct 2023 17:13:28 +0900 Subject: [PATCH 16/21] =?UTF-8?q?refactor:=20MemberPart=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../song/application/dto/MyPartsResponse.java | 2 +- .../src/main/resources/application-test.yml | 2 +- .../member_part/domain/MemberPartTest.java | 6 +- .../repository/MemberPartRepositoryTest.java | 23 ++++-- .../application/ArtistSearchServiceTest.java | 8 +- .../InMemorySongsSchedulerTest.java | 18 +--- .../song/application/SongServiceTest.java | 82 +++++++++---------- 7 files changed, 68 insertions(+), 73 deletions(-) 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/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/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 index 36230cff2..e61c8e4ad 100644 --- a/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java @@ -14,7 +14,6 @@ import org.springframework.test.context.jdbc.Sql; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; -import shook.shook.part.domain.PartLength; import shook.shook.song.application.dto.ArtistResponse; import shook.shook.song.application.dto.ArtistWithSongSearchResponse; import shook.shook.song.domain.Artist; @@ -33,6 +32,7 @@ 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 { @@ -159,9 +159,9 @@ private void saveSong(final Song song) { } private Song createNewSongWithArtist(final Artist artist) { - final KillingPart firstKillingPart = KillingPart.forSave(10, PartLength.SHORT); - final KillingPart secondKillingPart = KillingPart.forSave(15, PartLength.SHORT); - final KillingPart thirdKillingPart = KillingPart.forSave(20, PartLength.SHORT); + 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", 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 2f5b34ebf..b4c981b4f 100644 --- a/backend/src/test/java/shook/shook/song/application/SongServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java @@ -124,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( @@ -132,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() @@ -167,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( @@ -175,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() @@ -203,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); } @@ -316,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( @@ -324,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)) ); @@ -354,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); } @@ -398,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 내림차순으로 조회한다.") @@ -442,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)); } } @@ -490,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) ); } @@ -509,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() From 9ea320c278e2f80fa5900586431df575ee0cb8b8 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 19 Oct 2023 17:20:12 +0900 Subject: [PATCH 17/21] =?UTF-8?q?fix:=20dev=20data.sql=20=EC=95=84?= =?UTF-8?q?=ED=8B=B0=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/dev/data.sql | 253 ++++++++++++------------ 1 file changed, 129 insertions(+), 124 deletions(-) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index b34ab0ce5..852b4c3c7 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -2,29 +2,34 @@ TRUNCATE TABLE song; TRUNCATE TABLE voting_song; +TRUNCATE TABLE artist; -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 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 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()); @@ -35,8 +40,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) @@ -46,8 +51,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) @@ -57,8 +62,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) @@ -68,8 +73,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) @@ -79,8 +84,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) @@ -90,8 +95,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) @@ -101,8 +106,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) @@ -112,8 +117,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) @@ -123,8 +128,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) @@ -135,8 +140,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) @@ -146,8 +151,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) @@ -157,8 +162,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) @@ -168,8 +173,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) @@ -179,8 +184,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) @@ -190,8 +195,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) @@ -201,8 +206,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) @@ -212,8 +217,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) @@ -223,8 +228,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) @@ -234,8 +239,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) @@ -245,8 +250,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) @@ -256,8 +261,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) @@ -267,8 +272,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) @@ -278,8 +283,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()); @@ -290,8 +295,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) @@ -301,8 +306,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) @@ -312,8 +317,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) @@ -323,8 +328,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) @@ -334,8 +339,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) @@ -345,8 +350,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()); @@ -357,8 +362,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) @@ -368,8 +373,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) @@ -379,8 +384,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) @@ -390,8 +395,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) @@ -401,8 +406,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) @@ -412,8 +417,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) @@ -423,8 +428,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) @@ -434,8 +439,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) @@ -445,8 +450,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) @@ -456,8 +461,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) @@ -467,8 +472,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) @@ -478,8 +483,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) @@ -489,8 +494,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) @@ -500,8 +505,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) @@ -511,8 +516,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) @@ -522,8 +527,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) @@ -533,8 +538,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) @@ -544,8 +549,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) @@ -555,8 +560,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) @@ -566,8 +571,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) @@ -577,8 +582,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) @@ -588,8 +593,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) @@ -599,8 +604,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) @@ -610,8 +615,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) @@ -621,8 +626,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) @@ -632,8 +637,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) @@ -643,8 +648,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) @@ -654,8 +659,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) From fed4bc63db721eb7b2e6b6c0202698435fe4e520 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 19 Oct 2023 18:22:02 +0900 Subject: [PATCH 18/21] =?UTF-8?q?feat:=20=EC=9D=B8=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A6=AC=20=EC=95=84=ED=8B=B0=EC=8A=A4=ED=8A=B8=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/shook/song/application/ArtistSearchService.java | 6 ++++++ .../shook/shook/song/ui/ArtistSongSearchController.java | 8 ++++++++ backend/src/main/resources/dev/data.sql | 8 +++++++- .../shook/song/application/ArtistSearchServiceTest.java | 5 ++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java index c38bb94cf..ba72c315e 100644 --- a/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java +++ b/backend/src/main/java/shook/shook/song/application/ArtistSearchService.java @@ -11,6 +11,7 @@ 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; @@ -27,6 +28,7 @@ public class ArtistSearchService { 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); @@ -111,4 +113,8 @@ private Artist findArtistById(final long artistId) { Map.of("ArtistId", String.valueOf(artistId)) )); } + + public void updateArtistSynonymFromDatabase() { + generator.initialize(); + } } diff --git a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java index 16cfc36b2..d83ff5cc5 100644 --- a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java +++ b/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java @@ -5,6 +5,7 @@ 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.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -39,4 +40,11 @@ public ResponseEntity searchSongsByArtist( @PathVariable(name = "artist_id") final Long artistId) { return ResponseEntity.ok(artistSearchService.searchAllSongsByArtist(artistId)); } + + @PostMapping + public ResponseEntity updateArtistSynonym() { + artistSearchService.updateArtistSynonymFromDatabase(); + + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index 852b4c3c7..3418e80f2 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -5,9 +5,15 @@ 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 ('가수', '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 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', diff --git a/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java index e61c8e4ad..fb942d392 100644 --- a/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java @@ -20,6 +20,7 @@ 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; @@ -45,6 +46,8 @@ class ArtistSearchServiceTest extends UsingJpaTest { @Autowired private ArtistRepository artistRepository; + @Autowired + private InMemoryArtistSynonymsGenerator generator; @Autowired private KillingPartLikeRepository likeRepository; @@ -58,7 +61,7 @@ class ArtistSearchServiceTest extends UsingJpaTest { @BeforeEach void setUp() { artistSearchService = new ArtistSearchService(artistSynonyms, artistRepository, - songRepository); + songRepository, generator); final Song firstSong = songRepository.findById(1L).get(); final Song secondSong = songRepository.findById(2L).get(); final Song thirdSong = songRepository.findById(3L).get(); From 626b6457a7de0956892bec02d4aecea5ab5f34a9 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 19 Oct 2023 19:12:43 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20API?= =?UTF-8?q?=EC=99=80=20=EA=B0=80=EC=88=98=20API=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...hController.java => ArtistController.java} | 25 +---- .../shook/shook/song/ui/SearchController.java | 36 +++++++ .../shook/song/ui/openapi/ArtistApi.java | 32 ++++++ ...rtistSongSearchApi.java => SearchApi.java} | 47 +++----- .../auth/application/AuthServiceTest.java | 10 +- .../application/ArtistSearchServiceTest.java | 9 +- .../shook/song/ui/ArtistControllerTest.java | 101 ++++++++++++++++++ ...lerTest.java => SearchControllerTest.java} | 38 ++----- 8 files changed, 209 insertions(+), 89 deletions(-) rename backend/src/main/java/shook/shook/song/ui/{ArtistSongSearchController.java => ArtistController.java} (50%) create mode 100644 backend/src/main/java/shook/shook/song/ui/SearchController.java create mode 100644 backend/src/main/java/shook/shook/song/ui/openapi/ArtistApi.java rename backend/src/main/java/shook/shook/song/ui/openapi/{ArtistSongSearchApi.java => SearchApi.java} (57%) create mode 100644 backend/src/test/java/shook/shook/song/ui/ArtistControllerTest.java rename backend/src/test/java/shook/shook/song/ui/{ArtistSongSearchControllerTest.java => SearchControllerTest.java} (74%) diff --git a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java b/backend/src/main/java/shook/shook/song/ui/ArtistController.java similarity index 50% rename from backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java rename to backend/src/main/java/shook/shook/song/ui/ArtistController.java index d83ff5cc5..29fe06893 100644 --- a/backend/src/main/java/shook/shook/song/ui/ArtistSongSearchController.java +++ b/backend/src/main/java/shook/shook/song/ui/ArtistController.java @@ -1,47 +1,30 @@ 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.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; 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.application.dto.ArtistResponse; import shook.shook.song.application.dto.ArtistWithSongSearchResponse; -import shook.shook.song.ui.openapi.ArtistSongSearchApi; +import shook.shook.song.ui.openapi.ArtistApi; @RequiredArgsConstructor @RequestMapping("/singers") @RestController -public class ArtistSongSearchController implements ArtistSongSearchApi { +public class ArtistController implements ArtistApi { private final ArtistSearchService artistSearchService; - @GetMapping(params = {"name", "search=singer,song"}) - public ResponseEntity> searchArtistWithSongByKeyword( - @RequestParam(name = "search") final List searchTypes, - @RequestParam(name = "name") final String name) { - return ResponseEntity.ok(artistSearchService.searchArtistsAndTopSongsByKeyword(name)); - } - - @GetMapping(params = {"name", "search=singer"}) - public ResponseEntity> searchArtistByKeyword( - @RequestParam(name = "search") final String search, - @RequestParam(name = "name") final String name) { - return ResponseEntity.ok(artistSearchService.searchArtistsByKeyword(name)); - } - @GetMapping("/{artist_id}") public ResponseEntity searchSongsByArtist( @PathVariable(name = "artist_id") final Long artistId) { return ResponseEntity.ok(artistSearchService.searchAllSongsByArtist(artistId)); } - @PostMapping + @PutMapping("/synonyms") public ResponseEntity updateArtistSynonym() { artistSearchService.updateArtistSynonymFromDatabase(); 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..7fb5b537a --- /dev/null +++ b/backend/src/main/java/shook/shook/song/ui/SearchController.java @@ -0,0 +1,36 @@ +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.application.dto.ArtistResponse; +import shook.shook.song.application.dto.ArtistWithSongSearchResponse; +import shook.shook.song.ui.openapi.SearchApi; + +@RequiredArgsConstructor +@RequestMapping("/search") +@RestController +public class SearchController implements SearchApi { + + private final ArtistSearchService artistSearchService; + + @GetMapping(params = {"keyword", "type=singer,song"}) + public ResponseEntity> searchArtistWithSongByKeyword( + @RequestParam(name = "type") final List types, + @RequestParam(name = "keyword") final String keyword) { + return ResponseEntity.ok(artistSearchService.searchArtistsAndTopSongsByKeyword(keyword)); + } + + @GetMapping(params = {"keyword", "type=singer"}) + public ResponseEntity> searchArtistByKeyword( + @RequestParam(name = "type") final String type, + @RequestParam(name = "keyword") final String keyword) { + return ResponseEntity.ok(artistSearchService.searchArtistsByKeyword(keyword)); + } + +} 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/ArtistSongSearchApi.java b/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java similarity index 57% rename from backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java rename to backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java index 23be2bdee..8456ddee7 100644 --- a/backend/src/main/java/shook/shook/song/ui/openapi/ArtistSongSearchApi.java +++ b/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java @@ -8,13 +8,12 @@ import java.util.List; 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.RequestParam; import shook.shook.song.application.dto.ArtistResponse; import shook.shook.song.application.dto.ArtistWithSongSearchResponse; -@Tag(name = "Artist Search", description = "가수 이름 검색 API") -public interface ArtistSongSearchApi { +@Tag(name = "Singer Search", description = "가수 이름 검색 API") +public interface SearchApi { @Operation( summary = "검색어 입력 시 자동 완성되는 가수의 정보 검색", @@ -24,17 +23,17 @@ public interface ArtistSongSearchApi { responseCode = "200", description = "가수 검색 성공" ) - @GetMapping(value = "?search=artist", params = {"name"}) + @GetMapping(value = "?type=singer", params = {"keyword"}) ResponseEntity> searchArtistByKeyword( - @Parameter(name = "search", description = "검색 타입", - schema = @Schema(enumAsRef = true, allowableValues = {"artist"})) - @RequestParam(name = "search") final String search, + @Parameter(name = "type", description = "검색 타입", + schema = @Schema(enumAsRef = true, allowableValues = {"singer"})) + @RequestParam(name = "type") final String type, @Parameter( - name = "name", + name = "keyword", description = "검색할 가수 키워드", required = true ) - @RequestParam(name = "name") final String name + @RequestParam(name = "keyword") final String keyword ); @Operation( @@ -45,34 +44,16 @@ ResponseEntity> searchArtistByKeyword( responseCode = "200", description = "가수, TOP3 노래 검색 성공" ) - @GetMapping(value = "?search=artist,song", params = {"name"}) + @GetMapping(value = "?type=singer,song", params = {"keyword"}) ResponseEntity> searchArtistWithSongByKeyword( - @Parameter(name = "search", description = "검색 타입", - schema = @Schema(enumAsRef = true, allowableValues = {"artist", "song"})) - @RequestParam(name = "search") final List searchTypes, + @Parameter(name = "type", description = "검색 타입", + schema = @Schema(enumAsRef = true, allowableValues = {"singer", "song"})) + @RequestParam(name = "type") final List types, @Parameter( - name = "name", + name = "keyword", description = "검색할 가수 키워드", required = true ) - @RequestParam(name = "name") final String name - ); - - @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 + @RequestParam(name = "keyword") final String keyword ); } 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/song/application/ArtistSearchServiceTest.java b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java index fb942d392..f96d2bad5 100644 --- a/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/ArtistSearchServiceTest.java @@ -29,6 +29,7 @@ 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; @@ -39,6 +40,7 @@ class ArtistSearchServiceTest extends UsingJpaTest { private ArtistSearchService artistSearchService; private InMemoryArtistSynonyms artistSynonyms = new InMemoryArtistSynonyms(); + private InMemoryArtistSynonymsGenerator generator; @Autowired private SongRepository songRepository; @@ -46,9 +48,6 @@ class ArtistSearchServiceTest extends UsingJpaTest { @Autowired private ArtistRepository artistRepository; - @Autowired - private InMemoryArtistSynonymsGenerator generator; - @Autowired private KillingPartLikeRepository likeRepository; @@ -58,8 +57,12 @@ class ArtistSearchServiceTest extends UsingJpaTest { @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(); 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/ArtistSongSearchControllerTest.java b/backend/src/test/java/shook/shook/song/ui/SearchControllerTest.java similarity index 74% rename from backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java rename to backend/src/test/java/shook/shook/song/ui/SearchControllerTest.java index 0166163eb..97af62d1f 100644 --- a/backend/src/test/java/shook/shook/song/ui/ArtistSongSearchControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/SearchControllerTest.java @@ -23,12 +23,11 @@ import shook.shook.song.domain.InMemoryArtistSynonyms; import shook.shook.song.domain.Synonym; import shook.shook.song.domain.repository.ArtistRepository; -import shook.shook.song.domain.repository.ArtistSynonymRepository; @SuppressWarnings("NonAsciiCharacters") @Sql("classpath:/killingpart/initialize_killing_part_song.sql") @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class ArtistSongSearchControllerTest { +class SearchControllerTest { private Artist newJeans; private Artist 가수; @@ -60,13 +59,10 @@ void setUp() { @Autowired private InMemoryArtistSynonyms artistSynonyms; - @Autowired - private ArtistSynonymRepository synonymRepository; - @Autowired private ArtistRepository artistRepository; - @DisplayName("search=singer,song name=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하거나 끝나는 가수, 가수의 TOP3 노래 리스트를 반환한다.") + @DisplayName("type=singer,song keyword=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하거나 끝나는 가수, 가수의 TOP3 노래 리스트를 반환한다.") @Test void searchArtistWithSongByKeyword() { // given @@ -75,9 +71,9 @@ void searchArtistWithSongByKeyword() { // when final List response = RestAssured.given().log().all() - .params(Map.of("name", keyword, "search", searchType)) + .params(Map.of("keyword", keyword, "type", searchType)) .when().log().all() - .get("/singers") + .get("/search") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract() @@ -105,7 +101,7 @@ private List getSongIdsFromResponse(final ArtistWithSongSearchResponse res .toList(); } - @DisplayName("search=singer name=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하는 가수 목록을 반환한다.") + @DisplayName("type=singer keyword=검색어 으로 요청을 보내는 경우 상태코드 200, 검색어로 시작하는 가수 목록을 반환한다.") @Test void searchArtistByKeyword() { // given @@ -114,9 +110,9 @@ void searchArtistByKeyword() { // when final List response = RestAssured.given().log().all() - .params(Map.of("name", keyword, "search", searchType)) + .params(Map.of("keyword", keyword, "type", searchType)) .when().log().all() - .get("/singers") + .get("/search") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract() @@ -130,24 +126,4 @@ void searchArtistByKeyword() { () -> assertThat(response.get(1)).hasFieldOrPropertyWithValue("id", 가수.getId()) ); } - - @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); - } } From 73c0091585f221d054aca93621def9107d5bb618 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 19 Oct 2023 20:41:40 +0900 Subject: [PATCH 20/21] =?UTF-8?q?fix:=20song,=20singer=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/shook/song/ui/SearchController.java | 21 +++----- .../shook/song/ui/openapi/SearchApi.java | 49 ++++++------------- 2 files changed, 21 insertions(+), 49 deletions(-) diff --git a/backend/src/main/java/shook/shook/song/ui/SearchController.java b/backend/src/main/java/shook/shook/song/ui/SearchController.java index 7fb5b537a..86951f123 100644 --- a/backend/src/main/java/shook/shook/song/ui/SearchController.java +++ b/backend/src/main/java/shook/shook/song/ui/SearchController.java @@ -8,8 +8,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import shook.shook.song.application.ArtistSearchService; -import shook.shook.song.application.dto.ArtistResponse; -import shook.shook.song.application.dto.ArtistWithSongSearchResponse; import shook.shook.song.ui.openapi.SearchApi; @RequiredArgsConstructor @@ -19,18 +17,13 @@ public class SearchController implements SearchApi { private final ArtistSearchService artistSearchService; - @GetMapping(params = {"keyword", "type=singer,song"}) - public ResponseEntity> searchArtistWithSongByKeyword( - @RequestParam(name = "type") final List types, - @RequestParam(name = "keyword") final String keyword) { - return ResponseEntity.ok(artistSearchService.searchArtistsAndTopSongsByKeyword(keyword)); - } - - @GetMapping(params = {"keyword", "type=singer"}) - public ResponseEntity> searchArtistByKeyword( - @RequestParam(name = "type") final String type, - @RequestParam(name = "keyword") final String keyword) { + @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/SearchApi.java b/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java index 8456ddee7..b558075cd 100644 --- a/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java +++ b/backend/src/main/java/shook/shook/song/ui/openapi/SearchApi.java @@ -2,58 +2,37 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; -import shook.shook.song.application.dto.ArtistResponse; -import shook.shook.song.application.dto.ArtistWithSongSearchResponse; -@Tag(name = "Singer Search", description = "가수 이름 검색 API") +@Tag(name = "Search", description = "검색 API") public interface SearchApi { @Operation( - summary = "검색어 입력 시 자동 완성되는 가수의 정보 검색", - description = "검색어 입력 시, 검색어로 시작하는 가수의 정보를 검색한다." + summary = "가수, 또는 가수와 노래 조회", + description = "가수를 가나다 순으로 조회한다." ) @ApiResponse( responseCode = "200", - description = "가수 검색 성공" + description = "가수 정보, (노래 목록) 검색 성공" ) - @GetMapping(value = "?type=singer", params = {"keyword"}) - ResponseEntity> searchArtistByKeyword( - @Parameter(name = "type", description = "검색 타입", - schema = @Schema(enumAsRef = true, allowableValues = {"singer"})) - @RequestParam(name = "type") final String type, - @Parameter( - name = "keyword", - description = "검색할 가수 키워드", - required = true - ) - @RequestParam(name = "keyword") final String keyword - ); - - @Operation( - summary = "검색 시, 검색 결과 조회", - description = "검색 시, 검색어로 시작하거나 끝나는 가수와 해당 가수의 TOP3 노래가 조회된다." + @Parameter( + name = "type", + description = "검색 타입, singer&song OR singer", + required = true ) - @ApiResponse( - responseCode = "200", - description = "가수, TOP3 노래 검색 성공" + @Parameter( + name = "keyword", + description = "검색 키워드", + required = true ) - @GetMapping(value = "?type=singer,song", params = {"keyword"}) - ResponseEntity> searchArtistWithSongByKeyword( - @Parameter(name = "type", description = "검색 타입", - schema = @Schema(enumAsRef = true, allowableValues = {"singer", "song"})) + @GetMapping("") + ResponseEntity> search( @RequestParam(name = "type") final List types, - @Parameter( - name = "keyword", - description = "검색할 가수 키워드", - required = true - ) @RequestParam(name = "keyword") final String keyword ); } From e7c8130838bc0a7044f4565f182a37bc9ff5d602 Mon Sep 17 00:00:00 2001 From: somsom13 Date: Thu, 19 Oct 2023 20:50:41 +0900 Subject: [PATCH 21/21] =?UTF-8?q?data:=20artist,=20=EB=8F=99=EC=9D=98?= =?UTF-8?q?=EC=96=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/dev/data.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/main/resources/dev/data.sql b/backend/src/main/resources/dev/data.sql index 3418e80f2..56b78124d 100644 --- a/backend/src/main/resources/dev/data.sql +++ b/backend/src/main/resources/dev/data.sql @@ -14,6 +14,11 @@ 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, 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',