h-lab/src/test/java/com/hlab/yanalyst/domain/channel/PersonPicksTest.java
hehihoho3@gmail.com 88179bb620 feat: 숏폼 큐 파서 - 1단계 candidates JSON도 클립으로 인식
- Opal "숏폼 생성기 1단계" 출력(candidates JSON)을 N===== 구분자
  매칭 실패 시 폴백으로 파싱해 DONE 처리
- 후보 원본 JSON은 capcutJson 자리에 보존, 시간·경계·선정이유는
  durationTable 요약으로 표시
- 실전 픽스처 opal-step1-candidates-sample.txt 추가
- 낡은 테스트 정리: ChannelRole.acceptsShorts 제거 반영,
  PersonPicks 쇼츠 컷 180초 반영

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 17:04:06 +09:00

131 lines
5.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<PersonPicks.Found> 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<PersonPicks.Found> kept = PersonPicks.keep(found, Set.of(), null, 0);
assertThat(kept).extracting(PersonPicks.Found::videoId).containsExactly("c", "d");
}
@Test
void 길이를_모르면_버린다() {
// 상세 조회 실패로 길이를 못 채운 건 롱폼인지 알 수 없으므로 담지 않는다
List<PersonPicks.Found> kept = PersonPicks.keep(List.of(f("a", null, "C1", NOW)), Set.of(), null, 0);
assertThat(kept).isEmpty();
}
@Test
void 내채널과_경쟁채널_영상은_제외() {
List<PersonPicks.Found> found = List.of(
f("a", 300, "OWN_CH", NOW),
f("b", 300, "RIVAL_CH", NOW),
f("c", 300, "OTHER", NOW));
List<PersonPicks.Found> 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<PersonPicks.Found> found = List.of(
f("old", 300, "C1", cutoff.minusDays(1)),
f("new", 300, "C1", cutoff.plusDays(1)));
List<PersonPicks.Found> 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<PersonPicks.Found> 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<PersonPicks.Found> 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();
}
}