diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt index efbd497..131034e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt @@ -27,5 +27,6 @@ data class GetHomeResponse( val recommendChannelList: List, val freeContentList: List, val pointAvailableContentList: List, + val recommendContentList: List, val curationList: List ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt index 6941ac3..3b44d1e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt @@ -63,4 +63,20 @@ class HomeController(private val service: HomeService) { ) ) } + + // 추천 콘텐츠만 새로고침하기 위한 엔드포인트 + @GetMapping("/recommend-contents") + fun getRecommendContents( + @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, + @RequestParam("contentType", required = false) contentType: ContentType? = null, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + ApiResponse.ok( + service.getRecommendContentList( + isAdultContentVisible = isAdultContentVisible ?: true, + contentType = contentType ?: ContentType.ALL, + member = member + ) + ) + } } 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 b45db6e..7501745 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 @@ -48,6 +48,11 @@ class HomeService( @Value("\${cloud.aws.cloud-front.host}") private val imageHost: String ) { + companion object { + private const val RECOMMEND_TARGET_SIZE = 20 + private const val RECOMMEND_MAX_ATTEMPTS = 3 + } + fun fetchData( timezone: String, isAdultContentVisible: Boolean, @@ -213,6 +218,11 @@ class HomeService( recommendChannelList = recommendChannelList, freeContentList = freeContentList, pointAvailableContentList = pointAvailableContentList, + recommendContentList = getRecommendContentList( + isAdultContentVisible = isAdultContentVisible, + contentType = contentType, + member = member + ), curationList = curationList ) } @@ -284,4 +294,43 @@ class HomeService( return dayToSeriesPublishedDaysOfWeek[zonedDateTime.dayOfWeek] ?: SeriesPublishedDaysOfWeek.RANDOM } + + // 추천 콘텐츠 조회 로직은 변경 가능성을 고려하여 별도 메서드로 추출한다. + fun getRecommendContentList( + isAdultContentVisible: Boolean, + contentType: ContentType, + member: Member? + ): List { + val memberId = member?.id + val isAdult = member?.auth != null && isAdultContentVisible + + // 최대 3회까지 동일 로직으로 추가 조회하며, 중복을 제거하고 20개가 되면 조기 반환한다. + val unique = LinkedHashMap() // contentId 기준 중복 제거 + 순서 보존 + var attempt = 0 + while (attempt < RECOMMEND_MAX_ATTEMPTS && unique.size < RECOMMEND_TARGET_SIZE) { + attempt += 1 + val batch = contentService.getLatestContentByTheme( + theme = emptyList(), // 특정 테마에 종속되지 않도록 전체에서 랜덤 조회 + contentType = contentType, + isFree = false, + isAdult = isAdult, + orderByRandom = true + ).filter { + if (memberId != null) { + !memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) + } else { + true + } + } + + for (item in batch) { + if (unique.size >= RECOMMEND_TARGET_SIZE) break + if (!unique.containsKey(item.contentId)) { + unique[item.contentId] = item + } + } + } + + return unique.values.take(RECOMMEND_TARGET_SIZE) + } }