feat(discover): 발굴 일별 스케줄 연결 + 추천 채널 REST(목록/등록/제외/수동)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-06-26 10:44:27 +09:00
parent 02fa5be25b
commit 35944a8fd6
2 changed files with 72 additions and 0 deletions

View File

@ -0,0 +1,57 @@
package com.hlab.yanalyst.domain.channel;
import com.hlab.yanalyst.global.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/recommended-channels")
@RequiredArgsConstructor
@Tag(name = "Recommended Channel API", description = "자동 발굴된 추천 채널(떡상 Shorts)")
public class RecommendedChannelController {
private final RecommendedChannelRepository repository;
private final ChannelDiscoveryService discoveryService;
private final ChannelService channelService;
@GetMapping
@Operation(summary = "추천 채널 목록", description = "status=NEW 를 배율 내림차순으로 반환")
public ApiResponse<List<RecommendedChannel>> list() {
return ApiResponse.ok(repository.findByStatusOrderByRatioDesc("NEW"));
}
@PostMapping("/{id}/register")
@Operation(summary = "내 채널 등록", description = "추천 채널을 내 채널로 등록하고 REGISTERED 처리")
@Transactional
public ApiResponse<Void> register(@PathVariable Long id) {
RecommendedChannel rc = repository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("추천 채널을 찾을 수 없습니다: " + id));
channelService.saveChannelFromUrl("https://www.youtube.com/channel/" + rc.getChannelId());
rc.setStatus("REGISTERED");
repository.save(rc);
return ApiResponse.ok(null);
}
@PostMapping("/{id}/exclude")
@Operation(summary = "추천 제외", description = "다음 발굴에서 다시 뜨지 않게 EXCLUDED 처리")
@Transactional
public ApiResponse<Void> exclude(@PathVariable Long id) {
RecommendedChannel rc = repository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("추천 채널을 찾을 수 없습니다: " + id));
rc.setStatus("EXCLUDED");
repository.save(rc);
return ApiResponse.ok(null);
}
@PostMapping("/run")
@Operation(summary = "수동 발굴 실행", description = "스케줄과 동일한 발굴을 즉시 1회 실행")
public ApiResponse<Map<String, Object>> run() {
return ApiResponse.ok(discoveryService.runDiscovery());
}
}

View File

@ -25,6 +25,7 @@ public class ScheduledCollectionService {
private final ChannelRepository channelRepository;
private final ChannelService channelService;
private final YoutubeQuotaGuard quotaGuard;
private final com.hlab.yanalyst.domain.channel.ChannelDiscoveryService channelDiscoveryService;
@Value("${hlab.scheduler.channel-collection.enabled:true}")
private boolean enabled;
@ -32,6 +33,20 @@ public class ScheduledCollectionService {
@Value("${hlab.scheduler.channel-snapshot.enabled:true}")
private boolean snapshotEnabled;
@Value("${hlab.scheduler.channel-discovery.enabled:true}")
private boolean discoveryEnabled;
/** 추천 채널 발굴(지역 인기 Shorts → 떡상 채널). */
@Scheduled(cron = "${hlab.scheduler.channel-discovery.cron:0 30 4 * * *}")
public void scheduledDiscovery() {
if (!discoveryEnabled) {
log.info("[Scheduler] 추천 채널 발굴 비활성화됨 (hlab.scheduler.channel-discovery.enabled=false)");
return;
}
Map<String, Object> result = channelDiscoveryService.runDiscovery();
log.info("[Scheduler] 추천 채널 발굴 완료: {}", result);
}
/** 채널 1개 수집의 추정 쿼터 소비량(playlistItems + videos.list, 대략치). */
private static final long EST_UNITS_PER_CHANNEL = 12;