From 1e88036c42576f2a740b367ba877d50d76160ad1 Mon Sep 17 00:00:00 2001 From: andhl Date: Fri, 29 Sep 2023 20:32:58 +0900 Subject: [PATCH 1/8] song apis, playlist controller --- .DS_Store | Bin 0 -> 6148 bytes src/.DS_Store | Bin 0 -> 6148 bytes .../playlist/controller/PlaylistController.kt | 23 +++++---- .../playlist/repository/PlaylistEntity.kt | 4 ++ .../kotlin/song/controller/SongController.kt | 11 +++-- .../kotlin/song/repository/AlbumRepository.kt | 6 ++- src/main/kotlin/song/repository/SongEntity.kt | 25 ++++++++++ .../kotlin/song/repository/SongRepository.kt | 9 ++++ src/main/kotlin/song/service/SongArtists.kt | 7 +++ .../kotlin/song/service/SongServiceImpl.kt | 44 ++++++++++++++++-- 10 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store create mode 100644 src/main/kotlin/playlist/repository/PlaylistEntity.kt create mode 100644 src/main/kotlin/song/repository/SongEntity.kt create mode 100644 src/main/kotlin/song/repository/SongRepository.kt create mode 100644 src/main/kotlin/song/service/SongArtists.kt diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fa7ff019d625097a482b23f63e1a69f08c8dc744 GIT binary patch literal 6148 zcmeHLEl&eM5S^t38W2<<2sLL&5I}+_mXX#-T??kAv-SON}%!GOTy zFg26}1OkC1A-vh$w7b0)NeJvDyYFs3?#;bxcY7rwk!}^2iIPN=z+j9|U>aZ?=Mk|H z?U?}zzDBLQoGmo!%}AzmI0c*nzfl4H?#5|}%2c64>-(E(hi0ta2!edAk%uqQUR-T= z@^@=?y!dUrxo4~ma~YJ)!y46}!q^M}WcZP`O4tJlf!EUTZC zT(U;dPX)b{C};ITE4%2$h?;#oeV(1XDJ`swUJrvaEPlEH0T}YsLI*V}TKvK7l^r$t zPpe%WjQV<`$3Hm3;?Ep2{}?_6gP#VyZ}rE`9;zQl*RPxMSyYn z#!#VLIxxvs0AL($X*iZ^IM8DU(AO9$ga;;6DNvOP{fZ$}IsBf^^EHMFRXGXCjO%D- zq2Ex{4g$X?(@FRW-Ru-_3fKz7)M=9U|MlVT|8|i(a|$>G{*(eD9%O?wZb|R0k(=Ya v*2CDrVB@?{p)A3qw_|z0TQUEyAPq5(4}iYLP$4`p_d`I+;3lWQk1Fs5Rr|@2 literal 0 HcmV?d00001 diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c053acb2ac7956151e892864103bc0d8191b815e GIT binary patch literal 6148 zcmeHLJx{|h5IsYNNGu&0y)iKLADAjg-McbCXaz_OsYKZr@(YjEgWaHY1z-RuiAS zl1|R#O0J9NKX`r4eO=!)?YeJatLOLa-TdU~sPr$t^=~dSZVm%dQFkjVxsh#AS5~v! zm)CuG%<87xuK%n%<}d2X3+tK~1IBODo9jR9l882Dg-=Yz)}hJ=G+ zJUTGN764em90fXi3Fi2OA>p7155#FIP*YuMF`TBu?o(VyI4EkmxL6tcq{{Eji;LA^ z_c2^tP_)??Fa}Bnc6B-7{eO&CX0*vKQ*31n7z2Nd0arDPhJQMxyYm2_7asvw@EqBXPW5~0hzkh^Meag-Ob6PBKqSN_W8eoEcmpNtMXCS* literal 0 HcmV?d00001 diff --git a/src/main/kotlin/playlist/controller/PlaylistController.kt b/src/main/kotlin/playlist/controller/PlaylistController.kt index 07064a0..0b803cc 100644 --- a/src/main/kotlin/playlist/controller/PlaylistController.kt +++ b/src/main/kotlin/playlist/controller/PlaylistController.kt @@ -1,8 +1,6 @@ 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.Authenticated import com.wafflestudio.seminar.spring2023.user.service.User import org.springframework.http.ResponseEntity @@ -14,11 +12,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 +27,7 @@ class PlaylistController { @PathVariable id: Long, user: User?, ): PlaylistResponse { - TODO() + return PlaylistResponse(playlistService.get(id)) } @PostMapping("/api/v1/playlists/{id}/likes") @@ -34,7 +35,7 @@ class PlaylistController { @PathVariable id: Long, @Authenticated user: User, ) { - TODO() + playlistLikeService.create(playlistId = id, userId = user.id) } @DeleteMapping("/api/v1/playlists/{id}/likes") @@ -42,12 +43,16 @@ class PlaylistController { @PathVariable id: Long, @Authenticated user: User, ) { - TODO() + playlistLikeService.delete(playlistId = id, userId = user.id) } @ExceptionHandler fun handleException(e: PlaylistException): ResponseEntity { - TODO() + val status = when (e) { + is PlaylistAlreadyLikedException, is PlaylistNeverLikedException -> 409 + is PlaylistNotFoundException -> 404 + } + return ResponseEntity.status(status).build() } } diff --git a/src/main/kotlin/playlist/repository/PlaylistEntity.kt b/src/main/kotlin/playlist/repository/PlaylistEntity.kt new file mode 100644 index 0000000..e617ed4 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistEntity.kt @@ -0,0 +1,4 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +class PlaylistEntity { +} \ No newline at end of file diff --git a/src/main/kotlin/song/controller/SongController.kt b/src/main/kotlin/song/controller/SongController.kt index 6f1a865..928a40b 100644 --- a/src/main/kotlin/song/controller/SongController.kt +++ b/src/main/kotlin/song/controller/SongController.kt @@ -8,20 +8,23 @@ 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() + val songs = songService.search(keyword) + return SearchSongResponse(songs) } @GetMapping("/api/v1/albums") fun searchAlbum( @RequestParam keyword: String, ): SearchAlbumResponse { - TODO() + val albums = songService.searchAlbum(keyword) + return SearchAlbumResponse(albums) } } diff --git a/src/main/kotlin/song/repository/AlbumRepository.kt b/src/main/kotlin/song/repository/AlbumRepository.kt index e9a579e..146116c 100644 --- a/src/main/kotlin/song/repository/AlbumRepository.kt +++ b/src/main/kotlin/song/repository/AlbumRepository.kt @@ -1,5 +1,9 @@ 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 DISTINCT a FROM albums a LEFT JOIN FETCH a.artist ar WHERE a.title LIKE %:keyword%") + fun findAlbumsByTitleContaining(keyword: String): List +} \ 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..ea7271c --- /dev/null +++ b/src/main/kotlin/song/repository/SongEntity.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.seminar.spring2023.song.repository + +import com.wafflestudio.seminar.spring2023.song.service.Artist +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", referencedColumnName = "id") + val album: AlbumEntity, + + @ManyToMany + @JoinTable( + name = "song_artists", + joinColumns = [JoinColumn(name = "song_id")], + inverseJoinColumns = [JoinColumn(name = "artist_id")] + ) + val artists: 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 new file mode 100644 index 0000000..1aa9728 --- /dev/null +++ b/src/main/kotlin/song/repository/SongRepository.kt @@ -0,0 +1,9 @@ +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 DISTINCT s FROM songs s LEFT JOIN FETCH s.album a LEFT JOIN FETCH s.artists ar WHERE s.title LIKE %:keyword%") + fun findSongsByTitleContaining(keyword: String): List +} \ No newline at end of file diff --git a/src/main/kotlin/song/service/SongArtists.kt b/src/main/kotlin/song/service/SongArtists.kt new file mode 100644 index 0000000..a09aaef --- /dev/null +++ b/src/main/kotlin/song/service/SongArtists.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.seminar.spring2023.song.service + +data class SongArtists( + val id: Long, + val artist: Artist, + val song: Song, +) diff --git a/src/main/kotlin/song/service/SongServiceImpl.kt b/src/main/kotlin/song/service/SongServiceImpl.kt index 93ea395..5ae4201 100644 --- a/src/main/kotlin/song/service/SongServiceImpl.kt +++ b/src/main/kotlin/song/service/SongServiceImpl.kt @@ -1,15 +1,51 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.song.repository.AlbumEntity +import com.wafflestudio.seminar.spring2023.song.repository.AlbumRepository +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity +import com.wafflestudio.seminar.spring2023.song.repository.SongRepository import org.springframework.stereotype.Service @Service -class SongServiceImpl : SongService { - +class SongServiceImpl( + private val songRepository: SongRepository, + private val albumRepository: AlbumRepository + ) : SongService { override fun search(keyword: String): List { - TODO() + val songEntities = songRepository.findSongsByTitleContaining(keyword) + val songList = songEntities.map { mapSongEntityToSong(it) } + return songList.sortedBy { song -> song.title.length } } override fun searchAlbum(keyword: String): List { - TODO() + val albumEntities = albumRepository.findAlbumsByTitleContaining(keyword) + val albumList = albumEntities.map { mapAlbumEntityToAlbum(it) } + return albumList.sortedBy { album -> album.title.length } + } + + private fun mapSongEntityToSong(entity: SongEntity): Song { + val artistList = entity.artists.map { artistEntity -> + Artist(artistEntity.id, artistEntity.name) + } + + return Song( + id = entity.id, + title = entity.title, + artists = artistList, + album = entity.album.title, + image = entity.album.image, + duration = entity.duration + ) + } + + private fun mapAlbumEntityToAlbum(entity: AlbumEntity): Album { + val artist = Artist(entity.artist.id, entity.artist.name) + + return Album( + entity.id, + entity.title, + entity.image, + artist, + ) } } From 73f4c8af0558a4c6305645a445205e3b823da51f Mon Sep 17 00:00:00 2001 From: andhl Date: Sat, 30 Sep 2023 01:04:54 +0900 Subject: [PATCH 2/8] feat: PlaylistService --- src/main/kotlin/common/util/SongUtils.kt | 24 ++++++++++++++ .../playlist/controller/PlaylistController.kt | 5 ++- .../playlist/repository/PlaylistEntity.kt | 26 +++++++++++++-- .../repository/PlaylistGroupEntity.kt | 16 ++++++++++ .../repository/PlaylistGroupRepository.kt | 9 ++++++ .../playlist/repository/PlaylistRepository.kt | 10 ++++++ .../playlist/service/PlaylistServiceImpl.kt | 32 +++++++++++++++++-- src/main/kotlin/song/repository/SongEntity.kt | 2 +- .../kotlin/song/service/SongServiceImpl.kt | 19 ++--------- src/main/resources/application.yaml | 2 +- 10 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/common/util/SongUtils.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt create mode 100644 src/main/kotlin/playlist/repository/PlaylistRepository.kt diff --git a/src/main/kotlin/common/util/SongUtils.kt b/src/main/kotlin/common/util/SongUtils.kt new file mode 100644 index 0000000..7fb87e9 --- /dev/null +++ b/src/main/kotlin/common/util/SongUtils.kt @@ -0,0 +1,24 @@ +package com.wafflestudio.seminar.spring2023.common.util + +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity +import com.wafflestudio.seminar.spring2023.song.service.Artist +import com.wafflestudio.seminar.spring2023.song.service.Song + +class SongUtils { + companion object { + fun mapSongEntityToSong(entity: SongEntity): Song { + val artistList = entity.artists.map { artistEntity -> + Artist(artistEntity.id, artistEntity.name) + } + + return Song( + id = entity.id, + title = entity.title, + artists = artistList, + album = entity.album.title, + image = entity.album.image, + duration = entity.duration + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/playlist/controller/PlaylistController.kt b/src/main/kotlin/playlist/controller/PlaylistController.kt index 0b803cc..204f77c 100644 --- a/src/main/kotlin/playlist/controller/PlaylistController.kt +++ b/src/main/kotlin/playlist/controller/PlaylistController.kt @@ -27,7 +27,10 @@ class PlaylistController( @PathVariable id: Long, user: User?, ): PlaylistResponse { - return PlaylistResponse(playlistService.get(id)) + val playlist = playlistService.get(id) + val liked = if (user == null) false + else playlistLikeService.exists(playlistId = id, userId = user.id) + return PlaylistResponse(playlist, liked) } @PostMapping("/api/v1/playlists/{id}/likes") diff --git a/src/main/kotlin/playlist/repository/PlaylistEntity.kt b/src/main/kotlin/playlist/repository/PlaylistEntity.kt index e617ed4..2a609c2 100644 --- a/src/main/kotlin/playlist/repository/PlaylistEntity.kt +++ b/src/main/kotlin/playlist/repository/PlaylistEntity.kt @@ -1,4 +1,26 @@ package com.wafflestudio.seminar.spring2023.playlist.repository -class PlaylistEntity { -} \ No newline at end of file +import com.wafflestudio.seminar.spring2023.song.repository.SongEntity +import jakarta.persistence.* +import org.hibernate.annotations.BatchSize +import org.hibernate.annotations.Fetch +import org.hibernate.annotations.FetchMode +import org.springframework.web.bind.annotation.Mapping + +@Entity(name = "playlists") +class PlaylistEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val title: String, + val subtitle: String, + val image: String, + + @ManyToMany + @JoinTable( + name = "playlist_songs", + joinColumns = [JoinColumn(name = "playlist_id")], + inverseJoinColumns = [JoinColumn(name = "song_id")] + ) + val songs: Set, +) diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt b/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt new file mode 100644 index 0000000..77011db --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt @@ -0,0 +1,16 @@ +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 + @JoinColumn(name = "group_id") + val playlists: List +) diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt new file mode 100644 index 0000000..44d74b7 --- /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 pg FROM playlist_groups pg LEFT JOIN FETCH pg.playlists WHERE pg.open = true") + fun findOpenPlaylistGroups(): 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..31f7726 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistRepository.kt @@ -0,0 +1,10 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import org.springframework.data.jpa.repository.EntityGraph +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PlaylistRepository : JpaRepository { + @Query("SELECT DISTINCT p FROM playlists p LEFT JOIN FETCH p.songs s LEFT JOIN FETCH s.album a LEFT JOIN FETCH s.artists ar WHERE p.id = :id") + fun findPlaylistEntityById(id: Long): PlaylistEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt b/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt index f5bc5df..1afd136 100644 --- a/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistServiceImpl.kt @@ -1,17 +1,43 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.common.util.SongUtils +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistEntity +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistGroupRepository +import com.wafflestudio.seminar.spring2023.playlist.repository.PlaylistRepository +import jakarta.transaction.Transactional import org.springframework.context.annotation.Primary import org.springframework.stereotype.Service @Primary @Service -class PlaylistServiceImpl : PlaylistService { +class PlaylistServiceImpl( + private val playlistGroupRepository: PlaylistGroupRepository, + private val playlistRepository: PlaylistRepository + ) : PlaylistService { override fun getGroups(): List { - TODO() + val playlistGroups = playlistGroupRepository.findOpenPlaylistGroups() + return playlistGroups.filter { it.playlists.isNotEmpty() }.map { PlaylistGroup( + id = it.id, + title = it.title, + playlists = it.playlists.map { playlistEntity -> PlaylistBrief( + id = playlistEntity.id, + title = playlistEntity.title, + subtitle = playlistEntity.subtitle, + image = playlistEntity.image, + )} + )} } override fun get(id: Long): Playlist { - TODO() + val playlist = playlistRepository.findPlaylistEntityById(id) ?: throw PlaylistNotFoundException() + val songList = playlist.songs.map { SongUtils.mapSongEntityToSong(it) } + return Playlist( + id = playlist.id, + title = playlist.title, + subtitle = playlist.subtitle, + image = playlist.image, + songs = songList.sortedBy { it.id } + ) } } diff --git a/src/main/kotlin/song/repository/SongEntity.kt b/src/main/kotlin/song/repository/SongEntity.kt index ea7271c..1ab2645 100644 --- a/src/main/kotlin/song/repository/SongEntity.kt +++ b/src/main/kotlin/song/repository/SongEntity.kt @@ -21,5 +21,5 @@ class SongEntity ( joinColumns = [JoinColumn(name = "song_id")], inverseJoinColumns = [JoinColumn(name = "artist_id")] ) - val artists: List + val artists: Set ) \ No newline at end of file diff --git a/src/main/kotlin/song/service/SongServiceImpl.kt b/src/main/kotlin/song/service/SongServiceImpl.kt index 5ae4201..fd53f33 100644 --- a/src/main/kotlin/song/service/SongServiceImpl.kt +++ b/src/main/kotlin/song/service/SongServiceImpl.kt @@ -1,8 +1,8 @@ package com.wafflestudio.seminar.spring2023.song.service +import com.wafflestudio.seminar.spring2023.common.util.SongUtils import com.wafflestudio.seminar.spring2023.song.repository.AlbumEntity import com.wafflestudio.seminar.spring2023.song.repository.AlbumRepository -import com.wafflestudio.seminar.spring2023.song.repository.SongEntity import com.wafflestudio.seminar.spring2023.song.repository.SongRepository import org.springframework.stereotype.Service @@ -13,7 +13,7 @@ class SongServiceImpl( ) : SongService { override fun search(keyword: String): List { val songEntities = songRepository.findSongsByTitleContaining(keyword) - val songList = songEntities.map { mapSongEntityToSong(it) } + val songList = songEntities.map { SongUtils.mapSongEntityToSong(it) } return songList.sortedBy { song -> song.title.length } } @@ -23,21 +23,6 @@ class SongServiceImpl( return albumList.sortedBy { album -> album.title.length } } - private fun mapSongEntityToSong(entity: SongEntity): Song { - val artistList = entity.artists.map { artistEntity -> - Artist(artistEntity.id, artistEntity.name) - } - - return Song( - id = entity.id, - title = entity.title, - artists = artistList, - album = entity.album.title, - image = entity.album.image, - duration = entity.duration - ) - } - private fun mapAlbumEntityToAlbum(entity: AlbumEntity): Album { val artist = Artist(entity.artist.id, entity.artist.name) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ca2f880..6fd2177 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -13,4 +13,4 @@ spring: password: 1234 jpa: defer-datasource-initialization: true - show-sql: true \ No newline at end of file + show-sql: true From 31aa0a2df3a67c7e030a228f3e7149dee876fc9d Mon Sep 17 00:00:00 2001 From: andhl Date: Sat, 30 Sep 2023 22:33:39 +0900 Subject: [PATCH 3/8] feat: PlaylistLikesService --- .../repository/PlaylistLikesEntity.kt | 15 ++++++++ .../repository/PlaylistLikesRepository.kt | 9 +++++ .../service/PlaylistLikeServiceImpl.kt | 34 ++++++++++++++++--- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt 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 new file mode 100644 index 0000000..af4dca0 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.seminar.spring2023.playlist.repository + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity(name = "playlist_likes") +class PlaylistLikesEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + val playlist_id: Long, + val user_id: Long, +) diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt new file mode 100644 index 0000000..5575617 --- /dev/null +++ b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.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 PlaylistLikesRepository: JpaRepository { + @Query("SELECT p FROM playlist_likes p WHERE p.playlist_id = :playlistId and p.user_id = :userId") + fun findUserPlaylistLike(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..0e444c7 100644 --- a/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt @@ -1,19 +1,45 @@ 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 org.springframework.stereotype.Service @Service -class PlaylistLikeServiceImpl : PlaylistLikeService { +class PlaylistLikeServiceImpl( + private val playlistRepository: PlaylistRepository, + private val playlistLikesRepository: PlaylistLikesRepository +) : PlaylistLikeService { override fun exists(playlistId: Long, userId: Long): Boolean { - TODO() + if (!playlistRepository.existsById(playlistId)) { + throw PlaylistNotFoundException() + } + + return playlistLikesRepository.findUserPlaylistLike(playlistId, userId) != null } override fun create(playlistId: Long, userId: Long) { - TODO() + if (!playlistRepository.existsById(playlistId)) { + throw PlaylistNotFoundException() + } + + if (playlistLikesRepository.findUserPlaylistLike(playlistId, userId) != null) { + throw PlaylistAlreadyLikedException() + } + + playlistLikesRepository.save(PlaylistLikesEntity(playlist_id = playlistId, user_id = userId)) } override fun delete(playlistId: Long, userId: Long) { - TODO() + if (!playlistRepository.existsById(playlistId)) { + throw PlaylistNotFoundException() + } + + if (playlistLikesRepository.findUserPlaylistLike(playlistId, userId) == null) { + throw PlaylistNeverLikedException() + } + + playlistLikesRepository.delete(PlaylistLikesEntity(playlist_id = playlistId, user_id = userId)) } } From df18a3d719196a6405cb8f62b8a59ec5913bee66 Mon Sep 17 00:00:00 2001 From: andhl Date: Sun, 1 Oct 2023 00:05:04 +0900 Subject: [PATCH 4/8] feat: Playlist Cache --- src/main/kotlin/common/util/CacheUtils.kt | 34 +++++++++++++++++++ .../service/PlaylistServiceCacheImpl.kt | 20 +++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/common/util/CacheUtils.kt diff --git a/src/main/kotlin/common/util/CacheUtils.kt b/src/main/kotlin/common/util/CacheUtils.kt new file mode 100644 index 0000000..88da2b9 --- /dev/null +++ b/src/main/kotlin/common/util/CacheUtils.kt @@ -0,0 +1,34 @@ +package com.wafflestudio.seminar.spring2023.common.util + +import java.util.concurrent.ConcurrentHashMap + +class CacheUtils( + private val TTL: Long + ) +{ + private val cache = ConcurrentHashMap>() + + fun get(key: K): V? { + return if (cache[key] == null) { + null + } else if (cache[key]!!.isExpired()) { + cache.remove(key) + null + } else { + cache[key]!!.value + } + } + + fun put(key: K, value: V) { + cache[key] = CacheEntry(value, System.currentTimeMillis() + TTL) + } + + class CacheEntry( + val value: V, + private val expireAt: Long + ) { + fun isExpired(): Boolean { + return System.currentTimeMillis() > expireAt + } + } +} diff --git a/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt b/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt index 42c320c..9155faa 100644 --- a/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt +++ b/src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt @@ -1,5 +1,6 @@ package com.wafflestudio.seminar.spring2023.playlist.service +import com.wafflestudio.seminar.spring2023.common.util.CacheUtils import org.springframework.stereotype.Service // TODO: 캐시 TTL이 10초가 되도록 캐시 구현체를 구현 (추가 과제) @@ -7,12 +8,25 @@ import org.springframework.stereotype.Service class PlaylistServiceCacheImpl( private val impl: PlaylistServiceImpl, ) : PlaylistService { - + private val PLAYLIST_GROUP_KEY = "Playlist Group Key" + private val CACHE_TIME: Long = 10000L + private val playlistGroupListCache = CacheUtils>(CACHE_TIME) + private val playlistCache = CacheUtils(CACHE_TIME) override fun getGroups(): List { - TODO("Not yet implemented") + var groups = playlistGroupListCache.get(PLAYLIST_GROUP_KEY) + if (groups == null) { + groups = impl.getGroups() + playlistGroupListCache.put(PLAYLIST_GROUP_KEY, groups) + } + return groups } override fun get(id: Long): Playlist { - TODO("Not yet implemented") + var playlist = playlistCache.get(id) + if (playlist == null) { + playlist = impl.get(id) + playlistCache.put(id, playlist) + } + return playlist } } From c58c9332225c31c20fca803fe6607f66ca325e21 Mon Sep 17 00:00:00 2001 From: andhl Date: Mon, 2 Oct 2023 15:34:41 +0900 Subject: [PATCH 5/8] Song & Playlist integration tests --- .DS_Store | Bin 6148 -> 6148 bytes src/.DS_Store | Bin 6148 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes src/test/.DS_Store | Bin 0 -> 6148 bytes .../playlist/PlaylistIntegrationTest.kt | 114 ++++++++++++++++++ src/test/kotlin/song/SongIntegrationTest.kt | 29 +++++ 6 files changed, 143 insertions(+) create mode 100644 src/main/.DS_Store create mode 100644 src/test/.DS_Store diff --git a/.DS_Store b/.DS_Store index fa7ff019d625097a482b23f63e1a69f08c8dc744..fe8fdfc8f4f87b7ad3d31425525399c067a8aac5 100644 GIT binary patch delta 307 zcmZoMXffEJ%2F?*AIreNz`~%%kj{|FP?DSP;*yk;p9B=+cw^2MZjo}-5mi0~uY5s< zVQ_MOZUIma1B2EC5XlZUyNDr?A%!7_AvGr*NrgVs+m(}-v1Heq9{?$31uD%1DobTZ zL{Y|I12Pav7TKB4_FmrUn+kR&iVB27_3u2Z@xf4m?9|B>`Flh7^VzhSZ#NBo$IjtGFgFW67?U2J2u2D$N8cOJztz zQO01C1~dps7TKAP|1F>P9^y$wn{Ii*z<1aq|J5x}F diff --git a/src/.DS_Store b/src/.DS_Store index c053acb2ac7956151e892864103bc0d8191b815e..44a1c71325a1a737530df4bf04ae570993db89a3 100644 GIT binary patch delta 118 zcmZoMXffEJ%BZNJcY%R{frTNLA(0`IAul)I#U&{xKM5$tq5R{dr+eyTH%1$|2S7eh zQ3*pTLoq{1PP$=ma(-?BNRWYHy2#`Oj1p}30#1Qc?_-pLG4C--GchV`W@KVxn^?fM JnVsV=KL9>KAb|h? delta 107 zcmZoMXffEJ$|$7%{Tu@W0}DegLn1>aLtbva%j9rI*~uY{dU6gx37|{~Ln=ctLrG4$ zVQ_MOZUIP;fg#mt@&ZN)wuzIIYCK{l?_-pLG4C--GckPF%*e#XHnD(hGdss$egMnS B9T@-s diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..059e261073f933ed38625bf8d2bf25b5e77aa342 GIT binary patch literal 6148 zcmeHKu};H44E2QywPNYWcpHKN(Sbcw0mNL`QPT<#38{b%T~I!Ojep=9*bw3)_!*wh zR@;zNYzS4hjg|HuHJ-6q{qL64MC?fkOCpX+&%B*Uy2!rtCHKl_}eA4hec(XT#}>!tT3dd3#i zp?ezB19F*Smh!yvHtoE=Gm4`qxMOTqBzzz|vhU<>9Tm~$_|FcE^9PC z5UOm+d8uQ!&PxOD3t*)l?MZMl6-fsAh z49ME;(GAt~KouqH_b3~qHNLKkqO9r?_U!y=_4ZOeAE!RcAAL5foqdAc0~ymDUD7>r z*`l|Qe#dU=@Y>Dk_26iDdKPo)^!&{1d(T#WF+Fdb0cXG&=nVsU>Qc7fn;H2;oB?Ox zw-}J~AwUIl!$vV49cbbe062m<3i|TPC^5k>H*6F!0$~jWYAD+kgEbuXV1Bt_qp0D; zcKKlYGTRM>big_`^l&rQU vlapE-pchaP@oN-6LePn&7`{@951~ Date: Mon, 2 Oct 2023 15:38:52 +0900 Subject: [PATCH 6/8] remove .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 2 ++ src/.DS_Store | Bin 6148 -> 0 bytes src/main/.DS_Store | Bin 6148 -> 0 bytes src/test/.DS_Store | Bin 6148 -> 0 bytes 5 files changed, 2 insertions(+) delete mode 100644 .DS_Store delete mode 100644 src/.DS_Store delete mode 100644 src/main/.DS_Store delete mode 100644 src/test/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fe8fdfc8f4f87b7ad3d31425525399c067a8aac5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHLF-rq66n?Q)iw<^laJW$fEuD05IHlsEAXIQr!M0bb*wRvU5FG9&_zT=Sij#=o z;x0J2sGvg?)XCAs? zVm;b33=}*^xs=Wps+C$K(>a_1PJ!R30KdC&nxGPG(XRFWj-=MjSf!fDkAa-^Od3#@aA;h@sRkdFzy;fXY;~*i&zfXogwCQ{(1geofZ!Wu8j4T}k&W ztDeaVuBR9EY@wDS<*Zt0npZRxj_~>L^$mjNU!BBPRFIWApebFS+$NlQkN(_{Kfv`>%#Qn=LV@(DhCMr+`zS zr2wxF9vY*sF;pm*4s`Me0F1*e4cBrF2YT!P`Wi!p@W6x`3RFXdK4J(p9DYyd`5Hro zYB&kWjCnM(&?gkNgTU{}bP~Qo*ECryb3X*646btu{HOw- DYhA}D diff --git a/.gitignore b/.gitignore index 1b90726..933a314 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .idea/ .gradle/ build/ + +.DS_Store \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 44a1c71325a1a737530df4bf04ae570993db89a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu}T9$5S`I04s23bST5KIf{;Hr!-=sI(pqRtP$B09jGe`OfvsO;p_N}CeukxQ zcE;qETrHwAu={rB?d;q=xZN8f;=%o-Pt+r#98HjA&|%{4q&94^kSu%5uKJ_Px|(-v z@>^Z<>={*bK{s@pJpbOoTbrxuvK-IqF?{*%=gZ0Q^!YI5Crf_#IDQzOrTqN5rOo~a zLXmetQ@W!?l9!&g+gv;E`|0Z{uf{<#9Uv&Hf)Mekh! zSHKlmDj?@WKocws8^w5Zpo=8{FoQc9#!^d2OfW1A8$~cM)=;2^vXvOD;qV7rR~R;m z8cuA*2V3Uv;)P>&>>uKC;zH4TSHKlW71-2iPwxLCd@`Lyej4I4SHKncV+wFqj>-XE z%I?;+*OR+8pq-j`LB?2D7&eNsi}+(W(0>G?Al|tGKcK)D D+)qWw diff --git a/src/main/.DS_Store b/src/main/.DS_Store deleted file mode 100644 index 059e261073f933ed38625bf8d2bf25b5e77aa342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu};H44E2QywPNYWcpHKN(Sbcw0mNL`QPT<#38{b%T~I!Ojep=9*bw3)_!*wh zR@;zNYzS4hjg|HuHJ-6q{qL64MC?fkOCpX+&%B*Uy2!rtCHKl_}eA4hec(XT#}>!tT3dd3#i zp?ezB19F*Smh!yvHtoE=Gm4`qxMOTqBzzz|vhU<>9Tm~$_|FcE^9PC z5UOm+d8uQ!&PxOD3t*)l?MZMl6-fsAh z49ME;(GAt~KouqH_b3~qHNLKkqO9r?_U!y=_4ZOeAE!RcAAL5foqdAc0~ymDUD7>r z*`l|Qe#dU=@Y>Dk_26iDdKPo)^!&{1d(T#WF+Fdb0cXG&=nVsU>Qc7fn;H2;oB?Ox zw-}J~AwUIl!$vV49cbbe062m<3i|TPC^5k>H*6F!0$~jWYAD+kgEbuXV1Bt_qp0D; zcKKlYGTRM>big_`^l&rQU vlapE-pchaP@oN-6LePn&7`{@951~ Date: Mon, 2 Oct 2023 15:43:53 +0900 Subject: [PATCH 7/8] add trailing blank lines --- .gitignore | 2 +- src/main/kotlin/common/util/SongUtils.kt | 2 +- src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt | 2 +- src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt | 2 +- src/main/kotlin/playlist/repository/PlaylistRepository.kt | 2 +- src/main/kotlin/song/repository/SongEntity.kt | 2 +- src/main/kotlin/song/repository/SongRepository.kt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 933a314..7284b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ .gradle/ build/ -.DS_Store \ No newline at end of file +.DS_Store diff --git a/src/main/kotlin/common/util/SongUtils.kt b/src/main/kotlin/common/util/SongUtils.kt index 7fb87e9..3caab58 100644 --- a/src/main/kotlin/common/util/SongUtils.kt +++ b/src/main/kotlin/common/util/SongUtils.kt @@ -21,4 +21,4 @@ class SongUtils { ) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt index 44d74b7..3151838 100644 --- a/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt +++ b/src/main/kotlin/playlist/repository/PlaylistGroupRepository.kt @@ -6,4 +6,4 @@ import org.springframework.data.jpa.repository.Query interface PlaylistGroupRepository : JpaRepository { @Query("SELECT pg FROM playlist_groups pg LEFT JOIN FETCH pg.playlists WHERE pg.open = true") fun findOpenPlaylistGroups(): List -} \ No newline at end of file +} diff --git a/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt index 5575617..9bbb83b 100644 --- a/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt +++ b/src/main/kotlin/playlist/repository/PlaylistLikesRepository.kt @@ -6,4 +6,4 @@ import org.springframework.data.jpa.repository.Query interface PlaylistLikesRepository: JpaRepository { @Query("SELECT p FROM playlist_likes p WHERE p.playlist_id = :playlistId and p.user_id = :userId") fun findUserPlaylistLike(playlistId: Long, userId: Long): PlaylistLikesEntity? -} \ No newline at end of file +} diff --git a/src/main/kotlin/playlist/repository/PlaylistRepository.kt b/src/main/kotlin/playlist/repository/PlaylistRepository.kt index 31f7726..b412174 100644 --- a/src/main/kotlin/playlist/repository/PlaylistRepository.kt +++ b/src/main/kotlin/playlist/repository/PlaylistRepository.kt @@ -7,4 +7,4 @@ import org.springframework.data.jpa.repository.Query interface PlaylistRepository : JpaRepository { @Query("SELECT DISTINCT p FROM playlists p LEFT JOIN FETCH p.songs s LEFT JOIN FETCH s.album a LEFT JOIN FETCH s.artists ar WHERE p.id = :id") fun findPlaylistEntityById(id: Long): PlaylistEntity? -} \ 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 1ab2645..ff62b07 100644 --- a/src/main/kotlin/song/repository/SongEntity.kt +++ b/src/main/kotlin/song/repository/SongEntity.kt @@ -22,4 +22,4 @@ class SongEntity ( inverseJoinColumns = [JoinColumn(name = "artist_id")] ) val artists: Set -) \ 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 1aa9728..49b374c 100644 --- a/src/main/kotlin/song/repository/SongRepository.kt +++ b/src/main/kotlin/song/repository/SongRepository.kt @@ -6,4 +6,4 @@ import org.springframework.data.jpa.repository.Query interface SongRepository : JpaRepository { @Query("SELECT DISTINCT s FROM songs s LEFT JOIN FETCH s.album a LEFT JOIN FETCH s.artists ar WHERE s.title LIKE %:keyword%") fun findSongsByTitleContaining(keyword: String): List -} \ No newline at end of file +} From e8bc01679f831312700eaaaa3b4eaaaf22d89ca0 Mon Sep 17 00:00:00 2001 From: andhl Date: Mon, 2 Oct 2023 22:03:13 +0900 Subject: [PATCH 8/8] add @Transactional to integration tests --- src/test/kotlin/playlist/PlaylistIntegrationTest.kt | 2 ++ src/test/kotlin/song/SongIntegrationTest.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/test/kotlin/playlist/PlaylistIntegrationTest.kt b/src/test/kotlin/playlist/PlaylistIntegrationTest.kt index 1b3c5e7..472ac3b 100644 --- a/src/test/kotlin/playlist/PlaylistIntegrationTest.kt +++ b/src/test/kotlin/playlist/PlaylistIntegrationTest.kt @@ -1,6 +1,7 @@ package com.wafflestudio.seminar.spring2023.playlist import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -12,6 +13,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers @AutoConfigureMockMvc +@Transactional @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class PlaylistIntegrationTest @Autowired constructor( private val mvc: MockMvc, diff --git a/src/test/kotlin/song/SongIntegrationTest.kt b/src/test/kotlin/song/SongIntegrationTest.kt index ce274ed..32643b3 100644 --- a/src/test/kotlin/song/SongIntegrationTest.kt +++ b/src/test/kotlin/song/SongIntegrationTest.kt @@ -1,5 +1,6 @@ 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 @@ -10,6 +11,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers @AutoConfigureMockMvc +@Transactional @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SongIntegrationTest @Autowired constructor( private val mvc: MockMvc,