Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#498 Artist 엔티티 추가, 가수 검색 기능 구현 #502

Merged
merged 24 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8672aa6
feat: Artist 도메인 생성 및 Song singer -> artist 변경
somsom13 Oct 9, 2023
82fe989
feat: artist 테이블 생성 및 song, voting_song singer -> artist_id 변경
somsom13 Oct 10, 2023
df3248e
config: shook-security 스냅샷 최신화
somsom13 Oct 10, 2023
73036d1
feat: ArtistSynonym 엔티티 및 테이블 생성
somsom13 Oct 10, 2023
f97321d
feat: 가수 이름, 동의어 검색 기능 구현
somsom13 Oct 12, 2023
6cb2fe0
refactor: 코드리뷰 반영
somsom13 Oct 17, 2023
8768c5f
feat: 상세 가수 페이지 반환 시, 노래별 가수 이름 response 추가
somsom13 Oct 18, 2023
86deb04
refactor: 불필요한 주석, 개행 제거
somsom13 Oct 18, 2023
cd164ad
feat: Artist 도메인 생성 및 Song singer -> artist 변경
somsom13 Oct 9, 2023
84f436f
feat: artist 테이블 생성 및 song, voting_song singer -> artist_id 변경
somsom13 Oct 10, 2023
5cbb92e
feat: ArtistSynonym 엔티티 및 테이블 생성
somsom13 Oct 10, 2023
97891f8
feat: 가수 이름, 동의어 검색 기능 구현
somsom13 Oct 12, 2023
92b40da
refactor: 코드리뷰 반영
somsom13 Oct 17, 2023
054e5ad
feat: 상세 가수 페이지 반환 시, 노래별 가수 이름 response 추가
somsom13 Oct 18, 2023
1c475b5
refactor: 불필요한 주석, 개행 제거
somsom13 Oct 18, 2023
be42a90
Merge branch 'feat/#498' of https://github.com/woowacourse-teams/2023…
somsom13 Oct 19, 2023
8d98dad
refactor: MemberPart 기능 통합
somsom13 Oct 19, 2023
9ea320c
fix: dev data.sql 아티스트 추가
somsom13 Oct 19, 2023
fed4bc6
feat: 인메모리 아티스트 업데이트 API 추가
somsom13 Oct 19, 2023
626b645
refactor: 검색 API와 가수 API 분리
somsom13 Oct 19, 2023
73c0091
fix: song, singer 검색 조건 수정
somsom13 Oct 19, 2023
e7c8130
data: artist, 동의어 데이터 추가
somsom13 Oct 19, 2023
7a0f0c9
Merge branch 'main' into feat/#498
somsom13 Oct 19, 2023
da98088
Merge branch 'main' into feat/#498
somsom13 Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public enum ErrorCode {
CAN_NOT_READ_SONG_DATA_FILE(3011, "노래 데이터 파일을 읽을 수 없습니다."),
SONG_ALREADY_EXIST(3012, "등록하려는 노래가 이미 존재합니다."),
WRONG_GENRE_TYPE(3013, "잘못된 장르 타입입니다."),
EMPTY_ARTIST_PROFILE_URL(3014, "가수 프로필 이미지는 비어있을 수 없습니다."),
TOO_LONG_ARTIST_PROFILE_URL(3015, "가수 프로필 이미지URL은 65,356자를 넘길 수 없습니다."),
EMPTY_ARTIST_SYNONYM(3016, "가수 동의어는 비어있을 수 없습니다."),
TOO_LONG_ARTIST_SYNONYM(3017, "가수 동의어는 255자를 넘길 수 없습니다."),
ARTIST_NOT_EXIST(3018, "존재하지 않는 가수입니다."),


// 4000: 투표
VOTING_PART_START_LESS_THAN_ZERO(4001, "파트의 시작 초는 0보다 작을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,7 +56,8 @@ public ResponseEntity<ErrorResponse> handleTokenException(final CustomException
MemberException.class,
VotingSongException.class,
VotingSongPartException.PartNotExistException.class,
PartException.class
PartException.class,
ArtistException.class
})
public ResponseEntity<ErrorResponse> handleGlobalBadRequestException(final CustomException e) {
log.error(e.getErrorInfoLog());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ArtistResponse> searchArtistsByKeyword(final String keyword) {
final List<Artist> artists = findArtistsStartsWithKeyword(keyword);

return artists.stream()
.map(ArtistResponse::from)
.toList();
}

private List<Artist> findArtistsStartsWithKeyword(final String keyword) {
final List<Artist> artistsFoundByName = inMemoryArtistSynonyms.findAllArtistsNameStartsWith(
keyword);
final List<Artist> artistsFoundBySynonym = inMemoryArtistSynonyms.findAllArtistsHavingSynonymStartsWith(
keyword);

return removeDuplicateArtistResultAndSortByName(artistsFoundByName, artistsFoundBySynonym);
}

private List<Artist> removeDuplicateArtistResultAndSortByName(final List<Artist> firstResult,
final List<Artist> secondResult) {
return Stream.concat(firstResult.stream(), secondResult.stream())
somsom13 marked this conversation as resolved.
Show resolved Hide resolved
.distinct()
.sorted(Comparator.comparing(Artist::getArtistName))
somsom13 marked this conversation as resolved.
Show resolved Hide resolved
.toList();
}

public List<ArtistWithSongSearchResponse> searchArtistsAndTopSongsByKeyword(
final String keyword) {
final List<Artist> artists = findArtistsStartsOrEndsWithKeyword(keyword);

return artists.stream()
.map(artist -> ArtistWithSongSearchResponse.of(
artist,
getSongsOfArtistSortedByLikeCount(artist).size(),
getTopSongsOfArtist(artist))
)
.toList();
}

private List<Artist> findArtistsStartsOrEndsWithKeyword(final String keyword) {
final List<Artist> artistsFoundByName = inMemoryArtistSynonyms.findAllArtistsNameStartsOrEndsWith(
keyword);
final List<Artist> artistsFoundBySynonym = inMemoryArtistSynonyms.findAllArtistsHavingSynonymStartsOrEndsWith(
keyword);

return removeDuplicateArtistResultAndSortByName(artistsFoundByName, artistsFoundBySynonym);
}

private List<Song> getTopSongsOfArtist(final Artist artist) {
final List<Song> songs = getSongsOfArtistSortedByLikeCount(artist);
if (songs.size() < TOP_SONG_COUNT_OF_ARTIST) {
return songs;
}

return songs.subList(0, TOP_SONG_COUNT_OF_ARTIST);
}

private List<Song> getSongsOfArtistSortedByLikeCount(final Artist artist) {
final List<Song> all = songRepository.findAll();
somsom13 marked this conversation as resolved.
Show resolved Hide resolved
log.info("all song: {}", all);
somsom13 marked this conversation as resolved.
Show resolved Hide resolved
final List<SongTotalLikeCountDto> songsWithTotalLikeCount = songRepository.findAllSongsWithTotalLikeCountByArtist(
artist);

log.info("found song of artist: {}", songsWithTotalLikeCount);
somsom13 marked this conversation as resolved.
Show resolved Hide resolved
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<Song> songs = getSongsOfArtistSortedByLikeCount(artist);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지로 노래 정렬이 필요한지 궁금합니다! 검색 결과로 나오는 노래들도 좋아요 정렬이 꼭 필요할까요? 개인적인 생각으로는 무작위로 보여줘도 상관없을 것 같아서요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 스쿼드 내에서 처음에 결정할 때 가수의 노래를 좋아요 순으로 보여주려고 정했었는데요, 아마 정렬을 안하면 id 오름차순으로 반환이 되겠죠? id 오름차순이 더 나을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 중요한 로직이 아니라면 랜덤으로 나와도 상관 없을 것 같다는 생각을 했습니다! 정렬에도 오버헤드가 있기는 하니까요 🤔 바론 생각은 어떠신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(이전 스쿼드) 스플릿과의 회의 결과 전달드립니다!
현재는 데이터가 많지 않기 때문에 정렬에 소모되는 리소스가 거의 없을 것이라 생각합니다.
베로가 말씀해주신 것 처럼 순서가 중요하지 않을 것 같다는 의견에도 동의하지만, 데이터가 많지 않은 현재 상황에서는 정렬을 굳이 하지 않을 이유도 없을 것 같네요!

이 코멘트는 기억해두었다가 데이터가 많아지는 시점에 적용하도록 하겠습니다. 감사합니다 😄


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))
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,7 +94,9 @@ private Optional<Song> parseToSong(final Row currentRow) {
final Optional<KillingParts> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서는 저희가 알아서 가수가 중복으로 저장되지 않도록 주의해야 하는 부분이려나요? 🤔 확실히 주의가 필요하겠네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠.. 똑같은 이름의 가수가 똑같은 제목의 노래를 부르는 상황이.. 없겠죠? 이 부분 코드 작성할 때 중복을 체크하지 않기로 한 이유가 있었는데 정확히 기억이 안나네요 😂

같이 논의해보면 좋을 것 같아요!

final Song savedSong = songRepository.save(song);
killingPartRepository.saveAll(song.getKillingParts());
return savedSong;
Expand All @@ -68,8 +66,10 @@ public SongSwipeResponse findSongByIdForFirstSwipe(
) {
final Song currentSong = inMemorySongs.getSongById(songId);

final List<Song> beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT);
final List<Song> afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT);
final List<Song> beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong,
BEFORE_SONGS_COUNT);
final List<Song> afterSongs = inMemorySongs.getNextLikedSongs(currentSong,
AFTER_SONGS_COUNT);

return convertToSongSwipeResponse(memberInfo, currentSong, beforeSongs, afterSongs);
}
Expand All @@ -87,7 +87,8 @@ private SongSwipeResponse convertToSongSwipeResponse(
}

final Member member = findMemberById(memberInfo.getMemberId());
final List<Long> killingPartIds = killingPartLikeRepository.findLikedKillingPartIdsByMember(member);
final List<Long> killingPartIds = killingPartLikeRepository.findLikedKillingPartIdsByMember(
member);
return SongSwipeResponse.of(currentSong, beforeSongs, afterSongs, killingPartIds);
}

Expand All @@ -105,7 +106,8 @@ public List<SongResponse> findSongByIdForBeforeSwipe(
final MemberInfo memberInfo
) {
final Song currentSong = inMemorySongs.getSongById(songId);
final List<Song> beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT);
final List<Song> beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong,
BEFORE_SONGS_COUNT);

return convertToSongResponses(memberInfo, beforeSongs);
}
Expand Down Expand Up @@ -136,7 +138,8 @@ public List<SongResponse> findSongByIdForAfterSwipe(
final MemberInfo memberInfo
) {
final Song currentSong = inMemorySongs.getSongById(songId);
final List<Song> afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT);
final List<Song> afterSongs = inMemorySongs.getNextLikedSongs(currentSong,
AFTER_SONGS_COUNT);

return convertToSongResponses(memberInfo, afterSongs);
}
Expand All @@ -163,7 +166,8 @@ public SongSwipeResponse findSongsByGenreForSwipe(
final Song currentSong = inMemorySongs.getSongById(songId);
final List<Song> prevSongs = inMemorySongs.getPrevLikedSongByGenre(currentSong, genre,
BEFORE_SONGS_COUNT);
final List<Song> nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre, AFTER_SONGS_COUNT);
final List<Song> nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre,
AFTER_SONGS_COUNT);

final Authority authority = memberInfo.getAuthority();

Expand Down Expand Up @@ -197,7 +201,8 @@ public List<SongResponse> findNextSongsByGenre(
) {
final Genre genre = Genre.findByName(genreName);
final Song currentSong = inMemorySongs.getSongById(songId);
final List<Song> nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre, AFTER_SONGS_COUNT);
final List<Song> nextSongs = inMemorySongs.getNextLikedSongByGenre(currentSong, genre,
AFTER_SONGS_COUNT);

return convertToSongResponses(memberInfo, nextSongs);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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<SongSearchResponse> songs;

public static ArtistWithSongSearchResponse of(final Artist artist, final int totalSongCount,
final List<Song> songs) {
return new ArtistWithSongSearchResponse(
artist.getId(),
artist.getArtistName(),
artist.getProfileImageUrl(),
totalSongCount,
convertToSongSearchResponse(songs)
);
}

private static List<SongSearchResponse> convertToSongSearchResponse(final List<Song> songs) {
return songs.stream()
.map(SongSearchResponse::from)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static SongResponse of(final Song song, final List<Long> likedKillingPart
return new SongResponse(
song.getId(),
song.getTitle(),
song.getSinger(),
song.getArtistName(),
song.getLength(),
song.getVideoId(),
song.getAlbumCoverUrl(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading
Loading