Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.idea/
.gradle/
build/

.DS_Store
34 changes: 34 additions & 0 deletions src/main/kotlin/common/util/CacheUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.wafflestudio.seminar.spring2023.common.util

import java.util.concurrent.ConcurrentHashMap

class CacheUtils<K, V>(
private val TTL: Long
)
{
private val cache = ConcurrentHashMap<K, CacheEntry<V>>()

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<V>(
val value: V,
private val expireAt: Long
) {
fun isExpired(): Boolean {
return System.currentTimeMillis() > expireAt
}
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/common/util/SongUtils.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
26 changes: 17 additions & 9 deletions src/main/kotlin/playlist/controller/PlaylistController.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,40 +12,50 @@ 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}")
fun getPlaylist(
@PathVariable id: Long,
user: User?,
): PlaylistResponse {
TODO()
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")
fun likePlaylist(
@PathVariable id: Long,
@Authenticated user: User,
) {
TODO()
playlistLikeService.create(playlistId = id, userId = user.id)
}

@DeleteMapping("/api/v1/playlists/{id}/likes")
fun undoLikePlaylist(
@PathVariable id: Long,
@Authenticated user: User,
) {
TODO()
playlistLikeService.delete(playlistId = id, userId = user.id)
}

@ExceptionHandler
fun handleException(e: PlaylistException): ResponseEntity<Unit> {
TODO()
val status = when (e) {
is PlaylistAlreadyLikedException, is PlaylistNeverLikedException -> 409
is PlaylistNotFoundException -> 404
}
return ResponseEntity.status(status).build()
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/main/kotlin/playlist/repository/PlaylistEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.wafflestudio.seminar.spring2023.playlist.repository

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<SongEntity>,
)
16 changes: 16 additions & 0 deletions src/main/kotlin/playlist/repository/PlaylistGroupEntity.kt
Original file line number Diff line number Diff line change
@@ -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<PlaylistEntity>
)
Original file line number Diff line number Diff line change
@@ -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<PlaylistGroupEntity, Long> {
@Query("SELECT pg FROM playlist_groups pg LEFT JOIN FETCH pg.playlists WHERE pg.open = true")
fun findOpenPlaylistGroups(): List<PlaylistGroupEntity>
}
15 changes: 15 additions & 0 deletions src/main/kotlin/playlist/repository/PlaylistLikesEntity.kt
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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<PlaylistLikesEntity, Long> {
@Query("SELECT p FROM playlist_likes p WHERE p.playlist_id = :playlistId and p.user_id = :userId")
fun findUserPlaylistLike(playlistId: Long, userId: Long): PlaylistLikesEntity?
}
10 changes: 10 additions & 0 deletions src/main/kotlin/playlist/repository/PlaylistRepository.kt
Original file line number Diff line number Diff line change
@@ -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<PlaylistEntity, Long> {
@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?
}
34 changes: 30 additions & 4 deletions src/main/kotlin/playlist/service/PlaylistLikeServiceImpl.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}
20 changes: 17 additions & 3 deletions src/main/kotlin/playlist/service/PlaylistServiceCacheImpl.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
package com.wafflestudio.seminar.spring2023.playlist.service

import com.wafflestudio.seminar.spring2023.common.util.CacheUtils
import org.springframework.stereotype.Service

// TODO: 캐시 TTL이 10초가 되도록 캐시 구현체를 구현 (추가 과제)
@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<String, List<PlaylistGroup>>(CACHE_TIME)
private val playlistCache = CacheUtils<Long, Playlist>(CACHE_TIME)
override fun getGroups(): List<PlaylistGroup> {
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
}
}
32 changes: 29 additions & 3 deletions src/main/kotlin/playlist/service/PlaylistServiceImpl.kt
Original file line number Diff line number Diff line change
@@ -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<PlaylistGroup> {
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 }
)
}
}
11 changes: 7 additions & 4 deletions src/main/kotlin/song/controller/SongController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/song/repository/AlbumRepository.kt
Original file line number Diff line number Diff line change
@@ -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<AlbumEntity, Long>
interface AlbumRepository : JpaRepository<AlbumEntity, Long> {
@Query("SELECT DISTINCT a FROM albums a LEFT JOIN FETCH a.artist ar WHERE a.title LIKE %:keyword%")
fun findAlbumsByTitleContaining(keyword: String): List<AlbumEntity>
}
Loading