diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt index fb30d96..4f9e75e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt @@ -75,7 +75,13 @@ class HomeService( } .map { val followerCount = explorerQueryRepository.getNotificationUserIds(it.id!!).size - it.toExplorerSectionCreator(imageHost, followerCount) + val follow = if (memberId != null) { + explorerQueryRepository.isFollow(it.id!!, memberId = memberId) + } else { + false + } + + it.toExplorerSectionCreator(imageHost, follow, followerCount = followerCount) } val latestContentThemeList = contentThemeService.getActiveThemeOfContent( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiController.kt new file mode 100644 index 0000000..263db9f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiController.kt @@ -0,0 +1,33 @@ +package kr.co.vividnext.sodalive.api.live + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.content.ContentType +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/live") +class LiveApiController( + private val service: LiveApiService +) { + @GetMapping + fun fetchData( + @RequestParam timezone: String, + @RequestParam("contentType", required = false) contentType: ContentType? = null, + @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + ApiResponse.ok( + service.fetchData( + isAdultContentVisible = isAdultContentVisible ?: true, + contentType = contentType ?: ContentType.ALL, + timezone = timezone, + member = member + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt new file mode 100644 index 0000000..1bc1c8a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt @@ -0,0 +1,94 @@ +package kr.co.vividnext.sodalive.api.live + +import kr.co.vividnext.sodalive.content.AudioContentService +import kr.co.vividnext.sodalive.content.ContentType +import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService +import kr.co.vividnext.sodalive.live.recommend.LiveRecommendService +import kr.co.vividnext.sodalive.live.room.LiveRoomService +import kr.co.vividnext.sodalive.live.room.LiveRoomStatus +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service + +@Service +class LiveApiService( + private val liveService: LiveRoomService, + private val contentService: AudioContentService, + private val recommendService: LiveRecommendService, + private val creatorCommunityService: CreatorCommunityService, + + private val blockMemberRepository: BlockMemberRepository +) { + fun fetchData( + isAdultContentVisible: Boolean, + contentType: ContentType, + timezone: String, + member: Member? + ): LiveMainResponse { + val memberId = member?.id + val isAdult = member?.auth != null && isAdultContentVisible + + val liveOnAirRoomList = liveService.getRoomList( + dateString = null, + status = LiveRoomStatus.NOW, + isAdultContentVisible = isAdultContentVisible, + pageable = Pageable.ofSize(20), + member = member, + timezone = timezone + ) + + val communityPostList = if (memberId != null) { + creatorCommunityService.getLatestPostListFromCreatorsYouFollow( + timezone = timezone, + memberId = memberId, + isAdult = isAdult + ) + } else { + listOf() + } + + val recommendLiveList = recommendService.getRecommendLive(member) + + val latestFinishedLiveList = liveService.getLatestFinishedLive(member) + + val replayLive = contentService.getLatestContentByTheme( + theme = listOf("다시듣기"), + contentType = contentType, + isFree = false, + isAdult = isAdult + ) + .filter { content -> + if (memberId != null) { + !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = content.creatorId) + } else { + true + } + } + + val followingChannelList = if (memberId != null) { + recommendService.getFollowingChannelList(member) + } else { + listOf() + } + + val liveReservationRoomList = liveService.getRoomList( + dateString = null, + status = LiveRoomStatus.RESERVATION, + isAdultContentVisible = isAdultContentVisible, + pageable = Pageable.ofSize(10), + member = member, + timezone = timezone + ) + + return LiveMainResponse( + liveOnAirRoomList = liveOnAirRoomList, + communityPostList = communityPostList, + recommendLiveList = recommendLiveList, + latestFinishedLiveList = latestFinishedLiveList, + replayLive = replayLive, + followingChannelList = followingChannelList, + liveReservationRoomList = liveReservationRoomList + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveMainResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveMainResponse.kt new file mode 100644 index 0000000..a1e16bd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveMainResponse.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.api.live + +import kr.co.vividnext.sodalive.content.AudioContentMainItem +import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.GetCommunityPostListResponse +import kr.co.vividnext.sodalive.live.recommend.GetRecommendChannelResponse +import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse +import kr.co.vividnext.sodalive.live.room.GetLatestFinishedLiveResponse +import kr.co.vividnext.sodalive.live.room.GetRoomListResponse + +data class LiveMainResponse( + val liveOnAirRoomList: List, + val communityPostList: List, + val recommendLiveList: List, + val latestFinishedLiveList: List, + val replayLive: List, + val followingChannelList: List, + val liveReservationRoomList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt index cf2a581..04a6c2c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt @@ -96,6 +96,33 @@ class RedisConfig( ) ) + cacheConfigMap["cache_ttl_10_minutes"] = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + GenericJackson2JsonRedisSerializer() + ) + ) + + cacheConfigMap["cache_ttl_5_minutes"] = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(5)) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + GenericJackson2JsonRedisSerializer() + ) + ) + + cacheConfigMap["cache_ttl_3_minutes"] = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(3)) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + GenericJackson2JsonRedisSerializer() + ) + ) + return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(defaultCacheConfig) .withInitialCacheConfigurations(cacheConfigMap) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt index e6c2470..b317300 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -83,6 +83,7 @@ class SecurityConfig( .antMatchers("/api/home").permitAll() .antMatchers("/api/home/latest-content").permitAll() .antMatchers("/api/home/day-of-week-series").permitAll() + .antMatchers(HttpMethod.GET, "/api/live").permitAll() .antMatchers(HttpMethod.GET, "/faq").permitAll() .antMatchers(HttpMethod.GET, "/faq/category").permitAll() .antMatchers("/audition").permitAll() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index b651166..aa5aeb3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -939,6 +939,7 @@ class AudioContentService( return GenerateUrlResponse(contentUrl) } + @Transactional(readOnly = true) fun getLatestContentByTheme( theme: List, contentType: ContentType, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt index 0ff055c..1c890b7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt @@ -15,5 +15,6 @@ data class GetExplorerSectionCreatorResponse( val nickname: String, val tags: String, val profileImageUrl: String, + val follow: Boolean, val followerCount: Int ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt index a209742..6698959 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt @@ -441,6 +441,7 @@ class CreatorCommunityService( return GetCommunityPostCommentListResponse(totalCount = totalCount, items = commentList) } + @Transactional(readOnly = true) fun getLatestPostListFromCreatorsYouFollow( timezone: String, memberId: Long, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt index 67d86fa..066dbda 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -3,15 +3,21 @@ package kr.co.vividnext.sodalive.live.recommend import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import org.springframework.cache.annotation.Cacheable import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class LiveRecommendService( private val repository: LiveRecommendRepository, private val blockMemberRepository: BlockMemberRepository ) { - + @Transactional(readOnly = true) + @Cacheable( + cacheNames = ["cache_ttl_3_hours"], + key = "'getRecommendLive:' + (#member ?: 'guest')" + ) fun getRecommendLive(member: Member?): List { return repository.getRecommendLive( isBlocked = { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLatestFinishedLiveResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLatestFinishedLiveResponse.kt index 8955da6..e242ecb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLatestFinishedLiveResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLatestFinishedLiveResponse.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.live.room +import com.fasterxml.jackson.annotation.JsonProperty import com.querydsl.core.annotations.QueryProjection import kr.co.vividnext.sodalive.extensions.getTimeAgoString import java.time.LocalDateTime @@ -8,22 +9,19 @@ data class GetLatestFinishedLiveQueryResponse @QueryProjection constructor( val memberId: Long, val nickname: String, val profileImageUrl: String, - val title: String, val updatedAt: LocalDateTime ) data class GetLatestFinishedLiveResponse( - val memberId: Long, - val nickname: String, - val profileImageUrl: String, - val title: String, - val timeAgo: String + @JsonProperty("memberId") val memberId: Long, + @JsonProperty("nickname") val nickname: String, + @JsonProperty("profileImageUrl") val profileImageUrl: String, + @JsonProperty("timeAgo") val timeAgo: String ) { constructor(response: GetLatestFinishedLiveQueryResponse) : this( response.memberId, response.nickname, response.profileImageUrl, - response.title, response.updatedAt.getTimeAgoString() ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index 5967fb9..4006683 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -404,7 +404,6 @@ class LiveRoomQueryRepositoryImpl( member.id, member.nickname, member.profileImage.prepend("/").prepend(cloudFrontHost), - liveRoom.title, liveRoom.updatedAt ) ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index b8a6e27..953b542 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -56,6 +56,7 @@ import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull @@ -113,6 +114,7 @@ class LiveRoomService( ) { private val tokenLocks: MutableMap = mutableMapOf() + @Transactional(readOnly = true) fun getRoomList( dateString: String?, status: LiveRoomStatus, @@ -1297,6 +1299,11 @@ class LiveRoomService( ) } + @Transactional(readOnly = true) + @Cacheable( + cacheNames = ["cache_ttl_10_minutes"], + key = "'getLatestFinishedLive:' + (#member ?: 'guest')" + ) fun getLatestFinishedLive(member: Member?): List { return repository.getLatestFinishedLive() .filter { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index d4ca483..0a9ba25 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -123,7 +123,11 @@ data class Member( } } - fun toExplorerSectionCreator(imageHost: String, followerCount: Int = 0): GetExplorerSectionCreatorResponse { + fun toExplorerSectionCreator( + imageHost: String, + follow: Boolean = false, + followerCount: Int = 0 + ): GetExplorerSectionCreatorResponse { return GetExplorerSectionCreatorResponse( id = id!!, nickname = nickname, @@ -136,7 +140,8 @@ data class Member( } else { "$imageHost/profile/default-profile.png" }, - followerCount = followerCount + followerCount = followerCount, + follow = follow ) } }