From 539730bc9dd5670fc437f36adead637bc8c6c7f5 Mon Sep 17 00:00:00 2001 From: eastshine2741 Date: Sun, 1 Oct 2023 13:12:52 +0900 Subject: [PATCH 1/5] =?UTF-8?q?entity=20class=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playlist/repository/PlaylistEntity.kt | 20 +++++++++++++++++++ .../repository/PlaylistGroupEntity.kt | 14 +++++++++++++ .../repository/PlaylistLikesEntity.kt | 18 +++++++++++++++++ .../playlist/repository/PlaylistSongEntity.kt | 17 ++++++++++++++++ .../kotlin/song/repository/ArtistEntity.kt | 8 +++----- .../song/repository/SongArtistEntity.kt | 16 +++++++++++++++ src/main/kotlin/song/repository/SongEntity.kt | 20 +++++++++++++++++++ src/main/kotlin/user/repository/UserEntity.kt | 4 ++++ 8 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/playlist/repository/PlaylistEntity.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistSongEntity.kt create mode 100644 src/main/kotlin/song/repository/SongArtistEntity.kt create mode 100644 src/main/kotlin/song/repository/SongEntity.kt diff --git a/src/main/kotlin/playlist/repository/PlaylistEntity.kt b/src/main/kotlin/playlist/repository/PlaylistEntity.kt new file mode 100644 index 0000000..5d8acfe --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistEntity.kt @@ -0,0 +1,20 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import jakarta.persistence.* + +@Entity(name = "playlists") +class PlaylistEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val subtitle: String, + val image: String, + @ManyToOne + @JoinColumn(name = "group_id") + val group: PlaylistGroupEntity, + @OneToMany(mappedBy = "playlist") + val likeUsers: List, + @OneToMany(mappedBy = "playlist") + val songs: List, +) \ No newline at end of file diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt b/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt new file mode 100644 index 0000000..9dfd152 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import jakarta.persistence.* + +@Entity(name = "playlist_groups") +class PlaylistGroupEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val open: Boolean, + @OneToMany(mappedBy = "group") + val playlists: List, +) \ No newline at end of file diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt new file mode 100644 index 0000000..b336aa5 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt @@ -0,0 +1,18 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import com.wafflestudio.seminar.spring2023.playlist.service.Playlist +import com.wafflestudio.seminar.spring2023.user.repository.UserEntity +import jakarta.persistence.* + +@Entity(name = "playlist_likes") +class PlaylistLikesEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + @ManyToOne + @JoinColumn(name = "playlist_id") + val playlist: PlaylistEntity, + @ManyToOne + @JoinColumn(name = "user_id") + val user: UserEntity, +) \ No newline at end of file diff --git a/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt b/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt new file mode 100644 index 0000000..eec39af --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt @@ -0,0 +1,17 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity +import jakarta.persistence.* + +@Entity(name = "playlist_songs") +class PlaylistSongEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + @ManyToOne + @JoinColumn(name = "playlist_id") + val playlist: PlaylistEntity, + @ManyToOne + @JoinColumn(name = "song_id") + val song: SongEntity, +) \ No newline at end of file diff --git a/src/main/kotlin/song/repository/ArtistEntity.kt b/src/main/kotlin/song/repository/ArtistEntity.kt index d1952eb..ce4dc8c 100644 --- a/src/main/kotlin/song/repository/ArtistEntity.kt +++ b/src/main/kotlin/song/repository/ArtistEntity.kt @@ -1,10 +1,6 @@ package com.wafflestudio.seminar.spring2023.song.repository -import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id -import jakarta.persistence.OneToMany +import jakarta.persistence.* @Entity(name = "artists") class ArtistEntity( @@ -14,4 +10,6 @@ class ArtistEntity( val name: String, @OneToMany(mappedBy = "artist") val albums: List, + @OneToMany(mappedBy = "artist") + val songs: List, ) \ No newline at end of file diff --git a/src/main/kotlin/song/repository/SongArtistEntity.kt b/src/main/kotlin/song/repository/SongArtistEntity.kt new file mode 100644 index 0000000..ff0e66a --- /dev/null +++ b/src/main/kotlin/song/repository/SongArtistEntity.kt @@ -0,0 +1,16 @@ +package com.wafflestudio.seminar.spring2023.song.repository + +import jakarta.persistence.* + +@Entity(name = "song_artists") +class SongArtistEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + @ManyToOne + @JoinColumn(name = "song_id") + val song: SongEntity, + @ManyToOne + @JoinColumn(name = "artist_id") + val artist: ArtistEntity, +) \ No newline at end of file diff --git a/src/main/kotlin/song/repository/SongEntity.kt b/src/main/kotlin/song/repository/SongEntity.kt new file mode 100644 index 0000000..fe2eec8 --- /dev/null +++ b/src/main/kotlin/song/repository/SongEntity.kt @@ -0,0 +1,20 @@ +package com.wafflestudio.seminar.spring2023.song.repository + +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistSongEntity +import jakarta.persistence.* + +@Entity(name = "songs") +class SongEntity ( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val duration: String, + @ManyToOne + @JoinColumn(name = "album_id") + val album: AlbumEntity, + @OneToMany(mappedBy = "song") + val artists: List, + @OneToMany(mappedBy = "song") + val playlists: List, +) \ No newline at end of file diff --git a/src/main/kotlin/user/repository/UserEntity.kt b/src/main/kotlin/user/repository/UserEntity.kt index 6c0767c..07ddacf 100644 --- a/src/main/kotlin/user/repository/UserEntity.kt +++ b/src/main/kotlin/user/repository/UserEntity.kt @@ -1,9 +1,11 @@ package com.wafflestudio.seminar.spring2023.user.repository +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistLikesEntity import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue import jakarta.persistence.GenerationType import jakarta.persistence.Id +import jakarta.persistence.OneToMany @Entity(name = "users") class UserEntity( @@ -13,4 +15,6 @@ class UserEntity( val username: String, val password: String, val image: String, + @OneToMany(mappedBy = "user") + val likePlaylists: List ) From cb779ae5df9a556cd21c761b55c85ba6373ff311 Mon Sep 17 00:00:00 2001 From: eastshine2741 Date: Sun, 1 Oct 2023 18:18:57 +0900 Subject: [PATCH 2/5] =?UTF-8?q?PlaylistServiceTest=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playlist/repository/PlaylistEntity.kt | 2 +- .../repository/PlaylistGroupRepository.kt | 9 +++++++++ .../playlist/repository/PlaylistRepository.kt | 14 ++++++++++++++ .../playlist/repository/PlaylistSongEntity.kt | 4 ++-- src/main/kotlin/playlist/service/Playlist.kt | 14 +++++++++++++- .../kotlin/playlist/service/PlaylistBrief.kt | 11 ++++++++++- .../kotlin/playlist/service/PlaylistGroup.kt | 12 +++++++++++- .../playlist/service/PlaylistServiceImpl.kt | 18 ++++++++++++++---- src/main/kotlin/song/repository/SongEntity.kt | 2 +- .../kotlin/song/repository/SongRepository.kt | 15 +++++++++++++++ src/main/kotlin/song/service/Artist.kt | 9 ++++++++- src/main/kotlin/song/service/Song.kt | 13 ++++++++++++- .../kotlin/user/service/UserServiceImpl.kt | 3 ++- 13 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistRepository.kt create mode 100644 src/main/kotlin/song/repository/SongRepository.kt diff --git a/src/main/kotlin/playlist/repository/PlaylistEntity.kt b/src/main/kotlin/playlist/repository/PlaylistEntity.kt index 5d8acfe..cf9cd27 100644 --- a/src/main/kotlin/playlist/repository/PlaylistEntity.kt +++ b/src/main/kotlin/playlist/repository/PlaylistEntity.kt @@ -10,7 +10,7 @@ class PlaylistEntity( val title: String, val subtitle: String, val image: String, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id") val group: PlaylistGroupEntity, @OneToMany(mappedBy = "playlist") diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt new file mode 100644 index 0000000..b811882 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistGroupRepository: JpaRepository { + @Query("select a from playlist_groups a inner join fetch a.playlists where a.open = :open") + fun findNotEmptyGroupsByOpen(open: Boolean): List +} \ No newline at end of file diff --git a/src/main/kotlin/playlist/repository/PlaylistRepository.kt b/src/main/kotlin/playlist/repository/PlaylistRepository.kt new file mode 100644 index 0000000..3de9d9e --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistRepository.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistRepository : JpaRepository { + @Query(""" + select p from playlists p + left join fetch p.songs ps + join fetch ps.song s + where p.id = :id + """) + fun findByIdWithJoinFetch(id: Long): PlaylistEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt b/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt index eec39af..55e3ec5 100644 --- a/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt +++ b/src/main/kotlin/playlist/repository/PlaylistSongEntity.kt @@ -8,10 +8,10 @@ class PlaylistSongEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "playlist_id") val playlist: PlaylistEntity, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "song_id") val song: SongEntity, ) \ No newline at end of file diff --git a/src/main/kotlin/playlist/service/Playlist.kt b/src/main/kotlin/playlist/service/Playlist.kt index 7f00104..3a5f784 100644 --- a/src/main/kotlin/playlist/service/Playlist.kt +++ b/src/main/kotlin/playlist/service/Playlist.kt @@ -1,5 +1,7 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistEntity +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity import com.wafflestudio.seminar.spring2023.song.service.Song data class Playlist( @@ -8,4 +10,14 @@ data class Playlist( val subtitle: String, val image: String, val songs: List, -) \ No newline at end of file +) { + constructor(playlistEntity: PlaylistEntity, songEntities: List): this( + id = playlistEntity.id, + title = playlistEntity.title, + subtitle = playlistEntity.title, + image = playlistEntity.image, + songs = songEntities.map { + Song(it) + }.sortedBy { it.id } + ) +} \ No newline at end of file diff --git a/src/main/kotlin/playlist/service/PlaylistBrief.kt b/src/main/kotlin/playlist/service/PlaylistBrief.kt index dbff8d2..c1245b3 100644 --- a/src/main/kotlin/playlist/service/PlaylistBrief.kt +++ b/src/main/kotlin/playlist/service/PlaylistBrief.kt @@ -1,8 +1,17 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistEntity + data class PlaylistBrief( val id: Long, val title: String, val subtitle: String, val image: String, -) +) { + constructor(entity: PlaylistEntity): this( + id = entity.id, + title = entity.title, + subtitle = entity.subtitle, + image = entity.image + ) +} diff --git a/src/main/kotlin/playlist/service/PlaylistGroup.kt b/src/main/kotlin/playlist/service/PlaylistGroup.kt index 1f158d9..923eb63 100644 --- a/src/main/kotlin/playlist/service/PlaylistGroup.kt +++ b/src/main/kotlin/playlist/service/PlaylistGroup.kt @@ -1,7 +1,17 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistGroupEntity + data class PlaylistGroup( val id: Long, val title: String, val playlists: List, -) +) { + constructor(entity: PlaylistGroupEntity): this( + id = entity.id, + title = entity.title, + playlists = entity.playlists.map { + PlaylistBrief(it) + } + ) +} diff --git a/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt b/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt index f5bc5df..7ca6f4c 100644 --- a/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt @@ -1,17 +1,27 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.playlist.repository.* +import com.wafflestudio.seminar.spring2023.song.repository.SongRepository import org.springframework.context.annotation.Primary import org.springframework.stereotype.Service @Primary @Service -class PlaylistServiceImpl : PlaylistService { +class PlaylistServiceImpl( + val playlistGroupRepository: PlaylistGroupRepository, + val playlistRepository: PlaylistRepository, + val songRepository: SongRepository, +) : PlaylistService { override fun getGroups(): List { - TODO() + return playlistGroupRepository.findNotEmptyGroupsByOpen(true).map { + PlaylistGroup(it) + } } override fun get(id: Long): Playlist { - TODO() + val playlistEntity = playlistRepository.findByIdWithJoinFetch(id) ?: throw PlaylistNotFoundException() + val songEntities = songRepository.findByIdsWithJoinFetch(playlistEntity.songs.map { it.song.id }) + return Playlist(playlistEntity, songEntities) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/song/repository/SongEntity.kt b/src/main/kotlin/song/repository/SongEntity.kt index fe2eec8..a33aa86 100644 --- a/src/main/kotlin/song/repository/SongEntity.kt +++ b/src/main/kotlin/song/repository/SongEntity.kt @@ -10,7 +10,7 @@ class SongEntity ( val id: Long = 0L, val title: String, val duration: String, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "album_id") val album: AlbumEntity, @OneToMany(mappedBy = "song") diff --git a/src/main/kotlin/song/repository/SongRepository.kt b/src/main/kotlin/song/repository/SongRepository.kt new file mode 100644 index 0000000..d9be190 --- /dev/null +++ b/src/main/kotlin/song/repository/SongRepository.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.seminar.spring2023.song.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface SongRepository: JpaRepository { + @Query(""" + select s from songs s + join fetch s.album al + join fetch s.artists sa + join fetch sa.artist + where s.id in :ids + """) + fun findByIdsWithJoinFetch(ids: List): List +} \ No newline at end of file diff --git a/src/main/kotlin/song/service/Artist.kt b/src/main/kotlin/song/service/Artist.kt index eebfb57..ed3a3d1 100644 --- a/src/main/kotlin/song/service/Artist.kt +++ b/src/main/kotlin/song/service/Artist.kt @@ -1,6 +1,13 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.song.repository.ArtistEntity + data class Artist( val id: Long, val name: String, -) +) { + constructor(entity: ArtistEntity): this( + id = entity.id, + name = entity.name, + ) +} diff --git a/src/main/kotlin/song/service/Song.kt b/src/main/kotlin/song/service/Song.kt index cf2c818..1e5b835 100644 --- a/src/main/kotlin/song/service/Song.kt +++ b/src/main/kotlin/song/service/Song.kt @@ -1,5 +1,7 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity + data class Song( val id: Long, val title: String, @@ -7,4 +9,13 @@ data class Song( val album: String, val image: String, val duration: String, -) +) { + constructor(entity: SongEntity): this( + id = entity.id, + title = entity.title, + artists = entity.artists.map { Artist(it.artist) }, + album = entity.album.title, + image = entity.album.image, + duration = entity.duration + ) +} diff --git a/src/main/kotlin/user/service/UserServiceImpl.kt b/src/main/kotlin/user/service/UserServiceImpl.kt index 63077fd..0a01bd5 100644 --- a/src/main/kotlin/user/service/UserServiceImpl.kt +++ b/src/main/kotlin/user/service/UserServiceImpl.kt @@ -26,7 +26,8 @@ class UserServiceImpl( UserEntity( username = username, password = password, - image = image + image = image, + likePlaylists = emptyList() ) ) From fa861a2a335fede01b9cee3c8f318203496e895a Mon Sep 17 00:00:00 2001 From: eastshine2741 Date: Mon, 2 Oct 2023 11:19:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?PlaylistLikeServiceTest=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/PlaylistLikesEntity.kt | 4 +-- .../repository/PlaylistLikesRepository.kt | 12 +++++++ .../service/PlaylistLikeServiceImpl.kt | 33 ++++++++++++++++--- .../kotlin/user/controller/UserController.kt | 13 ++------ src/main/kotlin/user/service/UserException.kt | 2 ++ 5 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt index b336aa5..1a68484 100644 --- a/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt +++ b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt @@ -9,10 +9,10 @@ class PlaylistLikesEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "playlist_id") val playlist: PlaylistEntity, - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") val user: UserEntity, ) \ No newline at end of file diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt new file mode 100644 index 0000000..454d73c --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt @@ -0,0 +1,12 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistLikesRepository: JpaRepository { + @Query("select count(pl.id) > 0 from playlist_likes pl where pl.playlist.id=:playlistId and pl.user.id=:userId") + fun existsByPlaylistIdAndUserId(playlistId: Long, userId: Long): Boolean + + @Query("select pl from playlist_likes pl where pl.playlist.id=:playlistId and pl.user.id=:userId") + fun findByPlaylistIdAndUserId(playlistId: Long, userId: Long): PlaylistLikesEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt b/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt index d7e6d86..fdd5899 100644 --- a/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt @@ -1,19 +1,44 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistLikesEntity +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistLikesRepository +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistRepository +import com.wafflestudio.seminar.spring2023.user.repository.UserRepository +import com.wafflestudio.seminar.spring2023.user.service.UserNotFoundException import org.springframework.stereotype.Service @Service -class PlaylistLikeServiceImpl : PlaylistLikeService { +class PlaylistLikeServiceImpl( + val playlistLikesRepository: PlaylistLikesRepository, + val playlistRepository: PlaylistRepository, + val userRepository: UserRepository, +) : PlaylistLikeService { override fun exists(playlistId: Long, userId: Long): Boolean { - TODO() + return playlistLikesRepository.existsByPlaylistIdAndUserId(playlistId, userId) } override fun create(playlistId: Long, userId: Long) { - TODO() + if (exists(playlistId, userId)) throw PlaylistAlreadyLikedException() + val playlistEntity = playlistRepository.findById(playlistId).orElseThrow { + PlaylistNotFoundException() + } + val userEntity = userRepository.findById(userId).orElseThrow { + UserNotFoundException() + } + playlistLikesRepository.save( + PlaylistLikesEntity( + playlist = playlistEntity, + user = userEntity, + ) + ) } override fun delete(playlistId: Long, userId: Long) { - TODO() + val playlistLikesEntity = playlistLikesRepository.findByPlaylistIdAndUserId(playlistId, userId) + ?: throw PlaylistNeverLikedException() + playlistLikesRepository.delete( + playlistLikesEntity + ) } } diff --git a/src/main/kotlin/user/controller/UserController.kt b/src/main/kotlin/user/controller/UserController.kt index 8e374d3..0059a5c 100644 --- a/src/main/kotlin/user/controller/UserController.kt +++ b/src/main/kotlin/user/controller/UserController.kt @@ -1,15 +1,6 @@ package com.wafflestudio.seminar.spring2023.user.controller -import com.wafflestudio.seminar.spring2023.user.service.AuthenticateException -import com.wafflestudio.seminar.spring2023.user.service.Authenticated -import com.wafflestudio.seminar.spring2023.user.service.SignInInvalidPasswordException -import com.wafflestudio.seminar.spring2023.user.service.SignInUserNotFoundException -import com.wafflestudio.seminar.spring2023.user.service.SignUpBadPasswordException -import com.wafflestudio.seminar.spring2023.user.service.SignUpBadUsernameException -import com.wafflestudio.seminar.spring2023.user.service.SignUpUsernameConflictException -import com.wafflestudio.seminar.spring2023.user.service.User -import com.wafflestudio.seminar.spring2023.user.service.UserException -import com.wafflestudio.seminar.spring2023.user.service.UserService +import com.wafflestudio.seminar.spring2023.user.service.* import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.GetMapping @@ -60,7 +51,7 @@ class UserController( val status = when (e) { is SignUpBadUsernameException, is SignUpBadPasswordException -> 400 is SignUpUsernameConflictException -> 409 - is SignInUserNotFoundException, is SignInInvalidPasswordException -> 404 + is SignInUserNotFoundException, is SignInInvalidPasswordException, is UserNotFoundException -> 404 is AuthenticateException -> 401 } diff --git a/src/main/kotlin/user/service/UserException.kt b/src/main/kotlin/user/service/UserException.kt index 778c6b9..b9209f3 100644 --- a/src/main/kotlin/user/service/UserException.kt +++ b/src/main/kotlin/user/service/UserException.kt @@ -13,3 +13,5 @@ class SignInUserNotFoundException : UserException() class SignInInvalidPasswordException : UserException() class AuthenticateException : UserException() + +class UserNotFoundException: UserException() From 6a8d20c39332dbafcbf437800382dcf9efb6ad8e Mon Sep 17 00:00:00 2001 From: eastshine2741 Date: Mon, 2 Oct 2023 12:08:17 +0900 Subject: [PATCH 4/5] =?UTF-8?q?SongServiceTest=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/song/repository/AlbumRepository.kt | 10 +++++++++- src/main/kotlin/song/repository/SongRepository.kt | 10 ++++++++++ src/main/kotlin/song/service/Album.kt | 11 ++++++++++- src/main/kotlin/song/service/SongServiceImpl.kt | 11 ++++++++--- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/song/repository/AlbumRepository.kt b/src/main/kotlin/song/repository/AlbumRepository.kt index e9a579e..3322a3d 100644 --- a/src/main/kotlin/song/repository/AlbumRepository.kt +++ b/src/main/kotlin/song/repository/AlbumRepository.kt @@ -1,5 +1,13 @@ package com.wafflestudio.seminar.spring2023.song.repository import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query -interface AlbumRepository : JpaRepository \ No newline at end of file +interface AlbumRepository : JpaRepository { + @Query(""" + select a from albums a + join fetch a.artist ar + where a.title like %:keyword% order by length(a.title) asc + """) + fun findAllByTitleKeywordWithJoinFetch(keyword: String): List +} \ No newline at end of file diff --git a/src/main/kotlin/song/repository/SongRepository.kt b/src/main/kotlin/song/repository/SongRepository.kt index d9be190..80ed688 100644 --- a/src/main/kotlin/song/repository/SongRepository.kt +++ b/src/main/kotlin/song/repository/SongRepository.kt @@ -12,4 +12,14 @@ interface SongRepository: JpaRepository { where s.id in :ids """) fun findByIdsWithJoinFetch(ids: List): List + + @Query(""" + select s from songs s + join fetch s.album al + join fetch s.artists sa + join fetch sa.artist + where s.title like %:keyword% + order by length(s.title) asc + """) + fun findAllByTitleKeywordWithJoinFetch(keyword: String): List } \ No newline at end of file diff --git a/src/main/kotlin/song/service/Album.kt b/src/main/kotlin/song/service/Album.kt index a20e34d..be0e41a 100644 --- a/src/main/kotlin/song/service/Album.kt +++ b/src/main/kotlin/song/service/Album.kt @@ -1,8 +1,17 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.song.repository.AlbumEntity + data class Album( val id: Long, val title: String, val image: String, val artist: Artist, -) +) { + constructor(entity: AlbumEntity): this( + id = entity.id, + title = entity.title, + image = entity.image, + artist = Artist(entity.artist) + ) +} diff --git a/src/main/kotlin/song/service/SongServiceImpl.kt b/src/main/kotlin/song/service/SongServiceImpl.kt index 93ea395..cd32e0a 100644 --- a/src/main/kotlin/song/service/SongServiceImpl.kt +++ b/src/main/kotlin/song/service/SongServiceImpl.kt @@ -1,15 +1,20 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.song.repository.AlbumRepository +import com.wafflestudio.seminar.spring2023.song.repository.SongRepository import org.springframework.stereotype.Service @Service -class SongServiceImpl : SongService { +class SongServiceImpl( + val songRepository: SongRepository, + val albumRepository: AlbumRepository +) : SongService { override fun search(keyword: String): List { - TODO() + return songRepository.findAllByTitleKeywordWithJoinFetch(keyword).map { Song(it) } } override fun searchAlbum(keyword: String): List { - TODO() + return albumRepository.findAllByTitleKeywordWithJoinFetch(keyword).map { Album(it) } } } From 8e62010bbed3c943e097510af5216992850e5a8b Mon Sep 17 00:00:00 2001 From: eastshine2741 Date: Mon, 2 Oct 2023 14:32:06 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Controller=20=EC=9E=91=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20IntegrationTest=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playlist/controller/PlaylistController.kt | 39 ++++++-- .../kotlin/song/controller/SongController.kt | 8 +- .../playlist/PlaylistIntegrationTest.kt | 97 +++++++++++++++++++ src/test/kotlin/song/SongIntegrationTest.kt | 28 ++++++ 4 files changed, 160 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/playlist/controller/PlaylistController.kt b/src/main/kotlin/playlist/controller/PlaylistController.kt index 07064a0..478f3ba 100644 --- a/src/main/kotlin/playlist/controller/PlaylistController.kt +++ b/src/main/kotlin/playlist/controller/PlaylistController.kt @@ -1,10 +1,10 @@ package com.wafflestudio.seminar.spring2023.playlist.controller -import com.wafflestudio.seminar.spring2023.playlist.service.Playlist -import com.wafflestudio.seminar.spring2023.playlist.service.PlaylistException -import com.wafflestudio.seminar.spring2023.playlist.service.PlaylistGroup +import com.wafflestudio.seminar.spring2023.playlist.service.* +import com.wafflestudio.seminar.spring2023.user.service.AuthenticateException import com.wafflestudio.seminar.spring2023.user.service.Authenticated import com.wafflestudio.seminar.spring2023.user.service.User +import com.wafflestudio.seminar.spring2023.user.service.UserException import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.ExceptionHandler @@ -14,11 +14,14 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController @RestController -class PlaylistController { +class PlaylistController( + private val playlistService: PlaylistService, + private val playlistLikeService: PlaylistLikeService, +) { @GetMapping("/api/v1/playlist-groups") fun getPlaylistGroup(): PlaylistGroupsResponse { - TODO() + return PlaylistGroupsResponse(playlistService.getGroups()) } @GetMapping("/api/v1/playlists/{id}") @@ -26,7 +29,10 @@ class PlaylistController { @PathVariable id: Long, user: User?, ): PlaylistResponse { - TODO() + return PlaylistResponse( + playlist = playlistService.get(id), + isLiked = user?.let { playlistLikeService.exists(id, it.id) } ?: false + ) } @PostMapping("/api/v1/playlists/{id}/likes") @@ -34,7 +40,7 @@ class PlaylistController { @PathVariable id: Long, @Authenticated user: User, ) { - TODO() + playlistLikeService.create(id, user.id) } @DeleteMapping("/api/v1/playlists/{id}/likes") @@ -42,12 +48,27 @@ class PlaylistController { @PathVariable id: Long, @Authenticated user: User, ) { - TODO() + playlistLikeService.delete(id, user.id) + } + + @ExceptionHandler + fun handleException(e: UserException): ResponseEntity { + val status = when(e) { + is AuthenticateException -> 401 + else -> 400 + } + + return ResponseEntity.status(status).build() } @ExceptionHandler fun handleException(e: PlaylistException): ResponseEntity { - TODO() + val status = when (e) { + is PlaylistNotFoundException -> 404 + is PlaylistAlreadyLikedException, is PlaylistNeverLikedException -> 409 + } + + return ResponseEntity.status(status).build() } } diff --git a/src/main/kotlin/song/controller/SongController.kt b/src/main/kotlin/song/controller/SongController.kt index 6f1a865..03afcf0 100644 --- a/src/main/kotlin/song/controller/SongController.kt +++ b/src/main/kotlin/song/controller/SongController.kt @@ -8,20 +8,22 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController -class SongController { +class SongController( + private val songService: SongService +) { @GetMapping("/api/v1/songs") fun searchSong( @RequestParam keyword: String, ): SearchSongResponse { - TODO() + return SearchSongResponse(songService.search(keyword)) } @GetMapping("/api/v1/albums") fun searchAlbum( @RequestParam keyword: String, ): SearchAlbumResponse { - TODO() + return SearchAlbumResponse(songService.searchAlbum(keyword)) } } diff --git a/src/test/kotlin/playlist/PlaylistIntegrationTest.kt b/src/test/kotlin/playlist/PlaylistIntegrationTest.kt index 58de32b..311b401 100644 --- a/src/test/kotlin/playlist/PlaylistIntegrationTest.kt +++ b/src/test/kotlin/playlist/PlaylistIntegrationTest.kt @@ -1,13 +1,110 @@ package com.wafflestudio.seminar.spring2023.playlist +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional class PlaylistIntegrationTest @Autowired constructor( private val mvc: MockMvc, + private val mapper: ObjectMapper ) { + @BeforeEach + fun 회원가입() { + mvc.perform( + post("/api/v1/signup") + .content( + mapper.writeValueAsString( + mapOf( + "username" to "test", + "password" to "qwer1234", + "image" to "https://wafflestudio.com/images/icon_intro.svg" + ) + ) + ) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().`is`(200)) + } + + @Test + fun `플레이리스트 그룹 조회`() { + mvc.perform( + get("/api/v1/playlist-groups") + ).andExpect(status().`is`(200)) + } + + @Test + fun `플레이리스트 조회`() { + mvc.perform( + get("/api/v1/playlists/1") + ).andExpect(status().`is`(200)) + } + + @Test + fun `존재하지 않는 플레이리스트를 조회한 경우 404`() { + mvc.perform( + get("/api/v1/playlists/404404404") + ).andExpect(status().`is`(404)) + } + + @Test + fun `로그아웃 상태에서 플레이리스트 좋아요 되어있지 않아야 함`() { + mvc.perform( + get("/api/v1/playlists/1") + ).andExpect(jsonPath("$.isLiked").value(false)) + } + + @Test + fun `로그아웃 상태에서 플레이리스트 좋아요 등록 or 해제 시 401`() { + mvc.perform( + post("/api/v1/playlists/1/likes") + ).andExpect(status().`is`(401)) + } + + @Test + fun `로그인 상태에서 플레이리스트 좋아요 등록 or 해제`() { + mvc.perform( + post("/api/v1/playlists/1/likes") + .header("Authorization", "Bearer tset") + ).andExpect(status().`is`(200)) + + mvc.perform( + delete("/api/v1/playlists/1/likes") + .header("Authorization", "Bearer tset") + ).andExpect(status().`is`(200)) + } + + @Test + fun `로그인 상태에서 플레이리스트 중복 좋아요 등록 or 해제 시 409`() { + mvc.perform( + post("/api/v1/playlists/1/likes") + .header("Authorization", "Bearer tset") + ).andExpect(status().`is`(200)) + + mvc.perform( + post("/api/v1/playlists/1/likes") + .header("Authorization", "Bearer tset") + ).andExpect(status().`is`(409)) + + mvc.perform( + delete("/api/v1/playlists/1/likes") + .header("Authorization", "Bearer tset") + ).andExpect(status().`is`(200)) + + mvc.perform( + delete("/api/v1/playlists/1/likes") + .header("Authorization", "Bearer tset") + ).andExpect(status().`is`(409)) + } } diff --git a/src/test/kotlin/song/SongIntegrationTest.kt b/src/test/kotlin/song/SongIntegrationTest.kt index 0abe532..bd4453d 100644 --- a/src/test/kotlin/song/SongIntegrationTest.kt +++ b/src/test/kotlin/song/SongIntegrationTest.kt @@ -1,13 +1,41 @@ package com.wafflestudio.seminar.spring2023.song +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional class SongIntegrationTest @Autowired constructor( private val mvc: MockMvc, ) { + @Test + fun `제목에 키워드를 포함하는 곡 검색`() { + mvc.perform( + get("/api/v1/songs") + .param("keyword", "Shape") + ) + .andExpect(status().`is`(200)) + .andExpect(jsonPath("$.songs.length()").value(2)) + .andExpect(jsonPath("$.songs[0].title").value("Shapes")) + .andExpect(jsonPath("$.songs[1].title").value("Shape of You")) + } + + @Test + fun `제목에 키워드를 포함하는 앨범 검색`() { + mvc.perform( + get("/api/v1/albums") + .param("keyword", "GUT") + ) + .andExpect(status().`is`(200)) + .andExpect(jsonPath("$.albums.length()").value(1)) + .andExpect(jsonPath("$.albums[0].title").value("GUTS")) + } }