package com.hlab.yanalyst.domain.channel; import org.junit.jupiter.api.Test; import java.time.LocalDateTime; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; class PersonPicksTest { private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 2, 12, 0); private PersonPicks.Found f(String id, Integer durationSec, String channelId, LocalDateTime published) { return new PersonPicks.Found(id, "t_" + id, channelId, "ch_" + channelId, durationSec, published, 1000L); } @Test void 쇼츠는_버리고_롱폼만_남긴다() { List found = List.of( f("a", 30, "C1", NOW), // 쇼츠 f("b", 180, "C1", NOW), // 경계값 180초(유튜브 쇼츠 최대) = 쇼츠 f("c", 181, "C1", NOW), // 롱폼 f("d", 4300, "C1", NOW)); // 롱폼 List kept = PersonPicks.keep(found, Set.of(), null, 0); assertThat(kept).extracting(PersonPicks.Found::videoId).containsExactly("c", "d"); } @Test void 길이를_모르면_버린다() { // 상세 조회 실패로 길이를 못 채운 건 롱폼인지 알 수 없으므로 담지 않는다 List kept = PersonPicks.keep(List.of(f("a", null, "C1", NOW)), Set.of(), null, 0); assertThat(kept).isEmpty(); } @Test void 내채널과_경쟁채널_영상은_제외() { List found = List.of( f("a", 300, "OWN_CH", NOW), f("b", 300, "RIVAL_CH", NOW), f("c", 300, "OTHER", NOW)); List kept = PersonPicks.keep(found, Set.of("OWN_CH", "RIVAL_CH"), null, 0); assertThat(kept).extracting(PersonPicks.Found::videoId).containsExactly("c"); } @Test void 기간_이전_업로드는_제외() { LocalDateTime cutoff = NOW.minusDays(14); List found = List.of( f("old", 300, "C1", cutoff.minusDays(1)), f("new", 300, "C1", cutoff.plusDays(1))); List kept = PersonPicks.keep(found, Set.of(), cutoff, 0); assertThat(kept).extracting(PersonPicks.Found::videoId).containsExactly("new"); } @Test void 시간당_조회수_하한으로_팬채널_노이즈를_거른다() { // 실측: 팬채널·커버곡은 하루에 조회수 30회 미만, 방송사 클립은 시간당 수백~수천 LocalDateTime dayAgo = LocalDateTime.now().minusHours(24); PersonPicks.Found noise = new PersonPicks.Found("n", "팬캠", "C1", "흥미딘딘", 300, dayAgo, 16L); PersonPicks.Found signal = new PersonPicks.Found("s", "방송클립", "C2", "SBS", 300, dayAgo, 3000L); List kept = PersonPicks.keep(List.of(noise, signal), Set.of(), null, 20); assertThat(kept).extracting(PersonPicks.Found::videoId).containsExactly("s"); } @Test void 방금_올라온_영상은_조회수가_적어도_통과한다() { // 생 조회수로 자르면 선점 대상인 신선한 업로드를 놓친다 — 시간당으로 재기 때문에 통과해야 한다 LocalDateTime justNow = LocalDateTime.now().minusMinutes(30); PersonPicks.Found fresh = new PersonPicks.Found("f", "방금 올라옴", "C1", "KBS", 300, justNow, 50L); assertThat(PersonPicks.keep(List.of(fresh), Set.of(), null, 20)) .extracting(PersonPicks.Found::videoId).containsExactly("f"); } @Test void 조회수나_업로드일을_모르면_배제하지_않는다() { PersonPicks.Found unknown = new PersonPicks.Found("u", "t", "C1", "ch", 300, null, null); assertThat(PersonPicks.keep(List.of(unknown), Set.of(), null, 20)).hasSize(1); } @Test void 음악방송_무대_직캠_커버는_버린다() { // 아이돌 이름으로 검색하면 직캠·무대가 결과를 뒤덮는다. 말로 터지는 순간이 소재라 무대는 소용없다 assertThat(PersonPicks.isStageVideo("[#음중직캠] fromis_9 LEE CHAEYOUNG – Vitamin ME")).isTrue(); assertThat(PersonPicks.isStageVideo("[페이스캠4K] 프로미스나인 이채영")).isTrue(); assertThat(PersonPicks.isStageVideo("fromis_9 'Vitamin ME' PERFORMANCE VIDEO")).isTrue(); assertThat(PersonPicks.isStageVideo("[cover] 딘딘 - 태양은 가득히")).isTrue(); assertThat(PersonPicks.isStageVideo("교차편집 stage mix")).isTrue(); // 예능·드라마 클립은 남아야 한다 assertThat(PersonPicks.isStageVideo("이채영 임슬옹 공포체험 촬영 중단!?")).isFalse(); assertThat(PersonPicks.isStageVideo("남우조연상 - 윤경호 [제5회 청룡시리즈어워즈]")).isFalse(); assertThat(PersonPicks.isStageVideo(null)).isFalse(); } @Test void keep은_무대영상을_걸러낸다() { PersonPicks.Found stage = new PersonPicks.Found( "s", "[#음중직캠] 프로미스나인 이채영", "C1", "MBCkpop", 200, NOW, 50_000L); PersonPicks.Found variety = new PersonPicks.Found( "v", "이채영 임슬옹 공포체험 촬영 중단!?", "C2", "OngStyle", 200, NOW, 50_000L); assertThat(PersonPicks.keep(List.of(stage, variety), Set.of(), null, 0)) .extracting(PersonPicks.Found::videoId).containsExactly("v"); } @Test void null_입력과_빈_videoId_방어() { List found = new java.util.ArrayList<>(); found.add(null); found.add(f("", 300, "C1", NOW)); found.add(f("ok", 300, "C1", NOW)); assertThat(PersonPicks.keep(found, Set.of(), null, 0)) .extracting(PersonPicks.Found::videoId).containsExactly("ok"); assertThat(PersonPicks.keep(null, Set.of(), null, 0)).isEmpty(); } }