diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java b/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java new file mode 100644 index 0000000..b05cad7 --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java @@ -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() { + return ApiResponse.ok(repository.findByStatusOrderByRatioDesc("NEW")); + } + + @PostMapping("/{id}/register") + @Operation(summary = "내 채널 등록", description = "추천 채널을 내 채널로 등록하고 REGISTERED 처리") + @Transactional + public ApiResponse 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 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> run() { + return ApiResponse.ok(discoveryService.runDiscovery()); + } +} diff --git a/src/main/java/com/hlab/yanalyst/global/schedule/ScheduledCollectionService.java b/src/main/java/com/hlab/yanalyst/global/schedule/ScheduledCollectionService.java index 87f81b1..140be14 100644 --- a/src/main/java/com/hlab/yanalyst/global/schedule/ScheduledCollectionService.java +++ b/src/main/java/com/hlab/yanalyst/global/schedule/ScheduledCollectionService.java @@ -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 result = channelDiscoveryService.runDiscovery(); + log.info("[Scheduler] 추천 채널 발굴 완료: {}", result); + } + /** 채널 1개 수집의 추정 쿼터 소비량(playlistItems + videos.list, 대략치). */ private static final long EST_UNITS_PER_CHANNEL = 12;