From c0dbd3b23d090626ac969f568d6fe23f96cf0cce Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Sun, 2 Aug 2026 23:10:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=B8=EB=AC=BC=20=EC=B6=94=EC=A0=81?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9D=8C=EC=95=85=EB=B0=A9=EC=86=A1=20?= =?UTF-8?q?=EB=AC=B4=EB=8C=80=C2=B7=EC=A7=81=EC=BA=A0=C2=B7=EC=BB=A4?= =?UTF-8?q?=EB=B2=84=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 아이돌을 인물로 추적해봤더니 안 먹힌다는 걸 실측으로 확인했다. - 박지원 → 상위 3건이 전부 정치인 박지원(위대한 이슈 12.9만, KBC/JTV 뉴스). 동명이인 때문에 이름만으로는 못 쓴다. - 송하영·오해원·프로미스나인 → 잡히는 게 전부 음악방송 직캠·무대·팬캠. 예능 출연분이 직캠에 묻힌다. 배우·코미디언(윤경호·소지섭·딘딘)은 예능/드라마 클립이 상위에 오는데 아이돌은 검색 자체가 직캠 판이라 구조가 다르다. 그래서 두 가지를 했다. 1) 제목에 직캠·페이스캠·풀캠·교차편집·음중·PERFORMANCE VIDEO·커버 등이 들어가면 버린다. 말로 터지는 순간이 소재라 무대 영상은 소용없다. 적용 후 이채영 결과가 예능만 남았다(공포체험 4.9만, 고부갈등, 아이돌 인간극장). 2) 아이돌 소재는 인물이 아니라 채널로 잡는 게 맞다. fromis_9 공식채널을 SOURCE 시드로 추가 — 자체 예능 롱폼이 나온다([FM_1.24] 31분, Answer ME! 고민상담소 15분). 테스트 2건 추가 — 총 94건 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../yanalyst/domain/channel/PersonPicks.java | 24 ++++++++++++++++- .../domain/channel/PersonPicksTest.java | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/PersonPicks.java b/src/main/java/com/hlab/yanalyst/domain/channel/PersonPicks.java index 31b1fd1..d27a651 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/PersonPicks.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/PersonPicks.java @@ -13,8 +13,29 @@ import java.util.Set; */ public final class PersonPicks { + /** + * 제목에 이게 들어가면 버린다. 아이돌 이름으로 검색하면 음악방송 직캠·무대·교차편집이 + * 결과를 뒤덮어 정작 예능 출연분이 묻힌다. 말로 터지는 순간이 소재이므로 무대 영상은 소용없다. + */ + private static final List STAGE_MARKERS = List.of( + "직캠", "페이스캠", "풀캠", "fancam", "focus cam", + "교차편집", "stage mix", "stagemix", "무대교차", + "음중", "쇼챔", "엠카", "뮤직뱅크", "인기가요", "music bank", "show champion", + "performance video", "mv", "m/v", "lyric video", "cover", "커버" + ); + private PersonPicks() {} + /** 음악방송 무대·직캠·커버 영상인가(제목 기준). */ + public static boolean isStageVideo(String title) { + if (title == null || title.isBlank()) return false; + String t = title.toLowerCase(java.util.Locale.ROOT); + for (String marker : STAGE_MARKERS) { + if (t.contains(marker)) return true; + } + return false; + } + /** * 검색 결과 1건. * @@ -32,7 +53,7 @@ public final class PersonPicks { * @param minViewsPerHour 시간당 조회수 하한. 인물명 검색에는 팬채널·커버곡·개인 브이로그가 * 대량으로 딸려 오는데(실측 조회수 30회 미만), 생 조회수로 자르면 방금 올라온 * 영상까지 걸리므로 시간당 조회수로 거른다. 0 이하면 미적용 - * @return 롱폼이면서 제외 대상이 아닌 것들 (입력 순서 유지) + * @return 롱폼이면서 무대 영상이 아니고 제외 대상도 아닌 것들 (입력 순서 유지) */ public static List keep(List found, Set excludeChannels, LocalDateTime publishedAfter, double minViewsPerHour) { @@ -47,6 +68,7 @@ public final class PersonPicks { && excludeChannels.contains(f.ytChannelId())) continue; if (publishedAfter != null && f.publishedAt() != null && f.publishedAt().isBefore(publishedAfter)) continue; + if (isStageVideo(f.title())) continue; if (minViewsPerHour > 0 && !meetsVelocity(f, minViewsPerHour)) continue; out.add(f); } diff --git a/src/test/java/com/hlab/yanalyst/domain/channel/PersonPicksTest.java b/src/test/java/com/hlab/yanalyst/domain/channel/PersonPicksTest.java index c6d3d05..dbe0d76 100644 --- a/src/test/java/com/hlab/yanalyst/domain/channel/PersonPicksTest.java +++ b/src/test/java/com/hlab/yanalyst/domain/channel/PersonPicksTest.java @@ -90,6 +90,32 @@ class PersonPicksTest { 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<>();