feat: 라이브 메인 API
- 기존에 섹션별로 따로따로 호출하던 것을 하나로 합쳐서 호출할 수 있도록 API 추가
This commit is contained in:
		| @@ -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 | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,106 @@ | ||||
| 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.ExplorerQueryRepository | ||||
| 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, | ||||
|     private val explorerQueryRepository: ExplorerQueryRepository | ||||
| ) { | ||||
|     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) | ||||
|             .map { | ||||
|                 if (memberId != null) { | ||||
|                     it.isFollowing = explorerQueryRepository.getCreatorFollowing( | ||||
|                         creatorId = it.memberId, | ||||
|                         memberId = memberId | ||||
|                     )?.isFollow ?: false | ||||
|                 } | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|  | ||||
|         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 | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -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<GetRoomListResponse>, | ||||
|     val communityPostList: List<GetCommunityPostListResponse>, | ||||
|     val recommendLiveList: List<GetRecommendLiveResponse>, | ||||
|     val latestFinishedLiveList: List<GetLatestFinishedLiveResponse>, | ||||
|     val replayLive: List<AudioContentMainItem>, | ||||
|     val followingChannelList: List<GetRecommendChannelResponse>, | ||||
|     val liveReservationRoomList: List<GetRoomListResponse> | ||||
| ) | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -939,6 +939,11 @@ class AudioContentService( | ||||
|         return GenerateUrlResponse(contentUrl) | ||||
|     } | ||||
|  | ||||
|     @Transactional(readOnly = true) | ||||
|     @Cacheable( | ||||
|         cacheNames = ["default"], | ||||
|         key = "'getLatestContentByTheme:' + #theme + ':' + #contentType + ':' + #isAdult" | ||||
|     ) | ||||
|     fun getLatestContentByTheme( | ||||
|         theme: List<String>, | ||||
|         contentType: ContentType, | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository | ||||
| import kr.co.vividnext.sodalive.utils.generateFileName | ||||
| import kr.co.vividnext.sodalive.utils.validateImage | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.cache.annotation.Cacheable | ||||
| import org.springframework.context.ApplicationEventPublisher | ||||
| import org.springframework.data.repository.findByIdOrNull | ||||
| import org.springframework.stereotype.Service | ||||
| @@ -441,6 +442,11 @@ class CreatorCommunityService( | ||||
|         return GetCommunityPostCommentListResponse(totalCount = totalCount, items = commentList) | ||||
|     } | ||||
|  | ||||
|     @Transactional(readOnly = true) | ||||
|     @Cacheable( | ||||
|         cacheNames = ["cache_ttl_5_minutes"], | ||||
|         key = "'getLatestPostListFromCreatorsYouFollow:' + #memberId + ':' + #isAdult + ':' + #timezone" | ||||
|     ) | ||||
|     fun getLatestPostListFromCreatorsYouFollow( | ||||
|         timezone: String, | ||||
|         memberId: Long, | ||||
|   | ||||
| @@ -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<GetRecommendLiveResponse> { | ||||
|         return repository.getRecommendLive( | ||||
|             isBlocked = { | ||||
|   | ||||
| @@ -8,7 +8,6 @@ data class GetLatestFinishedLiveQueryResponse @QueryProjection constructor( | ||||
|     val memberId: Long, | ||||
|     val nickname: String, | ||||
|     val profileImageUrl: String, | ||||
|     val title: String, | ||||
|     val updatedAt: LocalDateTime | ||||
| ) | ||||
|  | ||||
| @@ -16,14 +15,13 @@ data class GetLatestFinishedLiveResponse( | ||||
|     val memberId: Long, | ||||
|     val nickname: String, | ||||
|     val profileImageUrl: String, | ||||
|     val title: String, | ||||
|     val timeAgo: String | ||||
|     val timeAgo: String, | ||||
|     var isFollowing: Boolean = false | ||||
| ) { | ||||
|     constructor(response: GetLatestFinishedLiveQueryResponse) : this( | ||||
|         response.memberId, | ||||
|         response.nickname, | ||||
|         response.profileImageUrl, | ||||
|         response.title, | ||||
|         response.updatedAt.getTimeAgoString() | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -404,7 +404,6 @@ class LiveRoomQueryRepositoryImpl( | ||||
|                     member.id, | ||||
|                     member.nickname, | ||||
|                     member.profileImage.prepend("/").prepend(cloudFrontHost), | ||||
|                     liveRoom.title, | ||||
|                     liveRoom.updatedAt | ||||
|                 ) | ||||
|             ) | ||||
|   | ||||
| @@ -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<Long, ReentrantReadWriteLock> = 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<GetLatestFinishedLiveResponse> { | ||||
|         return repository.getLatestFinishedLive() | ||||
|             .filter { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user