h-lab/src/main/java/com/hlab/yanalyst/domain/channel/FeedController.java

146 lines
7.4 KiB
Java

package com.hlab.yanalyst.domain.channel;
import com.hlab.yanalyst.domain.channel.dto.FeedItemDto;
import com.hlab.yanalyst.domain.channel.dto.FeedProgramDto;
import com.hlab.yanalyst.domain.channel.dto.FeedSeedDto;
import com.hlab.yanalyst.domain.channel.dto.TrackedPersonDto;
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.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/** 소재 발굴 피드 API. 소스(웹예능 롱폼) / 경쟁(예능짤 쇼츠) 신규 업로드를 최신순으로 제공한다. */
@RestController
@RequestMapping("/api/feed")
@RequiredArgsConstructor
@Tag(name = "Source Feed API", description = "소재 원본·경쟁 채널의 신규 업로드 피드")
public class FeedController {
private final FeedService feedService;
private final FeedCollectionService feedCollectionService;
private final SeedSuggestService seedSuggestService;
private final PersonCollectionService personCollectionService;
@GetMapping
@Operation(summary = "피드 조회",
description = "tab=SOURCE(롱폼 소재)|RIVAL(경쟁 쇼츠)|PERSON(인물 추적 롱폼). 항상 최신순.")
public ApiResponse<List<FeedItemDto>> feed(@RequestParam(defaultValue = "SOURCE") String tab,
@RequestParam(defaultValue = "ENTERTAINMENT") String topic,
@RequestParam(required = false) Integer days,
@RequestParam(required = false) String channelId,
@RequestParam(required = false) String lengthBucket,
@RequestParam(defaultValue = "false") boolean hideWorked,
@RequestParam(required = false) String person) {
return ApiResponse.ok(feedService.feed(tab, topic, days, channelId, lengthBucket, hideWorked, person));
}
// --- 인물 추적 ---
@GetMapping("/persons")
@Operation(summary = "추적 인물 목록", description = "인물별 담긴 영상 수와 마지막 수집 결과 포함")
public ApiResponse<List<TrackedPersonDto>> persons() {
return ApiResponse.ok(feedService.persons());
}
@PostMapping("/persons")
@Operation(summary = "추적 인물 추가", description = "1명당 검색 100 units 라 활성 인원에 상한이 있다")
public ApiResponse<TrackedPersonDto> addPerson(@RequestBody Map<String, String> body) {
return ApiResponse.created(feedService.addPerson(body.get("name")));
}
@PostMapping("/persons/{id}/toggle")
@Operation(summary = "추적 on/off", description = "끄면 수집 대상에서 빠지고 이미 담은 영상은 남는다")
public ApiResponse<Void> togglePerson(@PathVariable Long id, @RequestParam boolean enabled) {
feedService.togglePerson(id, enabled);
return ApiResponse.ok(null);
}
@DeleteMapping("/persons/{id}")
@Operation(summary = "추적 해제", description = "인물만 목록에서 지우고 담긴 영상은 남긴다")
public ApiResponse<Void> removePerson(@PathVariable Long id) {
feedService.removePerson(id);
return ApiResponse.ok(null);
}
@PostMapping("/persons/collect")
@Operation(summary = "인물 수집 실행", description = "활성 인물 전체를 즉시 1회 수집. 1명당 101 units.")
public ApiResponse<Map<String, Object>> collectPersons() {
return ApiResponse.ok(personCollectionService.collectAll());
}
@GetMapping("/programs")
@Operation(summary = "프로그램 목록", description = "피드 필터용 원본 채널 목록(영상 많은 순)")
public ApiResponse<List<FeedProgramDto>> programs(@RequestParam(defaultValue = "SOURCE") String tab,
@RequestParam(defaultValue = "ENTERTAINMENT") String topic) {
return ApiResponse.ok(feedService.programs(tab, topic));
}
@PostMapping("/collect")
@Operation(summary = "수동 수집", description = "스케줄과 동일한 피드 수집을 즉시 1회 실행")
public ApiResponse<Map<String, Object>> collect(
@RequestParam(defaultValue = "ENTERTAINMENT") String topic) {
return ApiResponse.ok(feedCollectionService.collect(FeedTopic.normalize(topic)));
}
@PostMapping("/refresh-views")
@Operation(summary = "소재 조회수 현행화", description = "저장된 피드 영상 조회수를 videos.list 50개 배치로 갱신한다")
public ApiResponse<Map<String, Object>> refreshViews(
@RequestParam(defaultValue = "ENTERTAINMENT") String topic) {
return ApiResponse.ok(feedCollectionService.refreshViews(FeedTopic.normalize(topic)));
}
// --- 시드(소스/경쟁 채널) 관리 ---
@GetMapping("/seeds")
@Operation(summary = "시드 목록", description = "등록된 소재 원본·경쟁 채널")
public ApiResponse<List<FeedSeedDto>> seeds(
@RequestParam(defaultValue = "ENTERTAINMENT") String topic) {
return ApiResponse.ok(feedService.seeds(topic));
}
@PostMapping("/seeds")
@Operation(summary = "시드 등록", description = "채널 URL 을 소재 원본(SOURCE) 또는 경쟁(RIVAL) 시드로 등록")
public ApiResponse<FeedSeedDto> addSeed(@RequestBody Map<String, String> body) {
String url = body.get("url");
if (url == null || url.isBlank()) throw new IllegalArgumentException("채널 URL 이 필요합니다.");
return ApiResponse.created(feedService.addSeed(url, body.getOrDefault("role", ChannelRole.SOURCE),
body.getOrDefault("topic", FeedTopic.ENTERTAINMENT.name()),
body.getOrDefault("format", FeedFormat.LONG_FORM.name())));
}
@DeleteMapping("/seeds/{id}")
@Operation(summary = "시드 해제", description = "채널과 그 채널에서 수집한 피드 영상을 함께 제거")
public ApiResponse<Void> removeSeed(@PathVariable Long id) {
feedService.removeSeed(id);
return ApiResponse.ok(null);
}
@PostMapping("/seeds/{id}/reset-failure")
@Operation(summary = "실패 카운터 초기화", description = "연속 실패로 자동 스킵 중인 시드를 다시 활성화")
public ApiResponse<Void> resetFailure(@PathVariable Long id) {
feedService.resetSeedFailure(id);
return ApiResponse.ok(null);
}
// --- 시드 자동 발굴(내 채널 역분석) ---
@GetMapping("/seeds/keywords")
@Operation(summary = "해시태그 분석", description = "내 채널 영상의 해시태그 빈도(쿼터 소비 없음)")
public ApiResponse<Map<String, Integer>> keywords() {
return ApiResponse.ok(seedSuggestService.analyzeKeywords());
}
@PostMapping("/seeds/suggest")
@Operation(summary = "소재 원본 후보 발굴",
description = "해시태그 상위 키워드로 공식채널을 검색해 추천 목록(roleHint=SOURCE)에 쌓는다. 키워드당 100 units 소비.")
public ApiResponse<Map<String, Object>> suggest(@RequestBody(required = false) Map<String, Object> body) {
@SuppressWarnings("unchecked")
List<String> keywords = body == null ? null : (List<String>) body.get("keywords");
return ApiResponse.ok(seedSuggestService.suggestSourceSeeds(keywords));
}
}