feat(home): 홈 추천 콘텐츠 조회 및 전용 엔드포인트 추가

- HomeService: getRecommendContentList 추가 및 fetchData에 recommendContentList 주입
- HomeController: GET /api/home/recommend-contents 엔드포인트 추가
- 추천 로직은 랜덤 20개, 성인/타입/차단 필터 반영
This commit is contained in:
2025-11-11 14:21:37 +09:00
parent 26c09de7c9
commit a538bb766d
3 changed files with 66 additions and 0 deletions

View File

@@ -27,5 +27,6 @@ data class GetHomeResponse(
val recommendChannelList: List<RecommendChannelResponse>,
val freeContentList: List<AudioContentMainItem>,
val pointAvailableContentList: List<AudioContentMainItem>,
val recommendContentList: List<AudioContentMainItem>,
val curationList: List<GetContentCurationResponse>
)

View File

@@ -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
)
)
}
}

View File

@@ -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<AudioContentMainItem> {
val memberId = member?.id
val isAdult = member?.auth != null && isAdultContentVisible
// 최대 3회까지 동일 로직으로 추가 조회하며, 중복을 제거하고 20개가 되면 조기 반환한다.
val unique = LinkedHashMap<Long, AudioContentMainItem>() // 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)
}
}