From 046c163e6f809e2b3855cabfcda48d4ffc154c79 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 21 Jul 2025 15:14:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20API=20-=20=EA=B8=B0=EC=A1=B4=EC=97=90=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=EB=B3=84=EB=A1=9C=20=EB=94=B0=EB=A1=9C=EB=94=B0?= =?UTF-8?q?=EB=A1=9C=20=ED=98=B8=EC=B6=9C=ED=95=98=EB=8D=98=20=EA=B2=83?= =?UTF-8?q?=EC=9D=84=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=95=A9=EC=B3=90?= =?UTF-8?q?=EC=84=9C=20=ED=98=B8=EC=B6=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/api/live/LiveApiController.kt | 33 ++++++ .../sodalive/api/live/LiveApiService.kt | 106 ++++++++++++++++++ .../sodalive/api/live/LiveMainResponse.kt | 18 +++ .../vividnext/sodalive/configs/RedisConfig.kt | 27 +++++ .../sodalive/configs/SecurityConfig.kt | 1 + .../sodalive/content/AudioContentService.kt | 5 + .../CreatorCommunityService.kt | 6 + .../live/recommend/LiveRecommendService.kt | 8 +- .../room/GetLatestFinishedLiveResponse.kt | 6 +- .../sodalive/live/room/LiveRoomRepository.kt | 1 - .../sodalive/live/room/LiveRoomService.kt | 7 ++ 11 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveMainResponse.kt 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..d7d60e2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt @@ -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 + ) + } +} 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..91476f6 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,11 @@ class AudioContentService( return GenerateUrlResponse(contentUrl) } + @Transactional(readOnly = true) + @Cacheable( + cacheNames = ["default"], + key = "'getLatestContentByTheme:' + #theme + ':' + #contentType + ':' + #isAdult" + ) fun getLatestContentByTheme( theme: List, contentType: ContentType, 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..c9820bc 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 @@ -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, 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..8f7f98f 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 @@ -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() ) } 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 {