- 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>
131 lines
5.8 KiB
Java
131 lines
5.8 KiB
Java
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();
|
||
}
|
||
}
|