From b8b119a94a3478f8061a3130f22e4f56e08a6df4 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 11 Aug 2026 12:38:07 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=8B=9C=EB=93=9C=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디글 클래식처럼 한 채널에 여러 프로그램이 섞인 경우 특정 프로그램만 피드에 수집하기 위한 설계. Channel에 feed_title_filter 컬럼을 추가하고 공백 무시 부분일치 OR 매칭으로 수집 단계에서 거른다. Co-Authored-By: Claude Opus 5 (1M context) --- ...026-08-11-feed-seed-title-filter-design.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-feed-seed-title-filter-design.md diff --git a/docs/superpowers/specs/2026-08-11-feed-seed-title-filter-design.md b/docs/superpowers/specs/2026-08-11-feed-seed-title-filter-design.md new file mode 100644 index 0000000..6fa2ee6 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-feed-seed-title-filter-design.md @@ -0,0 +1,135 @@ +# 시드 채널 제목 키워드 필터 설계 + +작성일: 2026-08-11 + +## 배경 + +소재 피드(`/feed`)의 시드는 **채널 단위**다(`FeedService.addSeed` → `Channel.role = SOURCE|RIVAL`). +디글 클래식(`@DiggleClassic`)처럼 한 채널이 여러 프로그램을 올리는 경우, 시드로 등록하면 +유 퀴즈 온 더 블럭뿐 아니라 놀면 뭐하니·코미디빅리그까지 전부 피드에 들어온다. + +피드 화면의 "프로그램" 드롭다운도 도움이 안 된다. `ChannelVideoRepository.feedPrograms`가 +`ytChannelId`로 그룹핑하기 때문에 드롭다운에는 "디글 클래식" 한 줄만 뜨고, +채널 **안에서** 특정 프로그램만 골라내는 수단이 없다. + +## 목표 + +시드 채널에 제목 키워드를 걸어, 그 키워드에 걸리는 영상만 피드에 수집한다. + +## 비목표 + +- 이미 수집된 영상의 소급 정리 (자동 삭제하지 않는다 — 아래 "알려진 동작" 참고) +- 프로그램 단위 그룹핑/드롭다운 개편 (채널 단위 그룹핑을 유지한다) +- 인물 추적(`PersonCollectionService`) 경로 변경 + +## 설계 + +### 1. 데이터 + +`Channel` 엔티티에 컬럼 하나를 추가한다. + +```java +/** 피드 수집 시 제목에 걸어둘 키워드(콤마 구분). 비어있으면 필터 없음 = 전부 수집. */ +@Column(name = "feed_title_filter", length = 255) +private String feedTitleFilter; +``` + +- `ddl-auto: update`라 기존 행은 `null`이 되고, `null`/공백은 "필터 없음"으로 해석된다. + 따라서 **기존 시드의 동작은 그대로**다. +- 변경 메서드: `Channel.changeFeedTitleFilter(String)` — 공백 문자열은 `null`로 정규화한다. + +### 2. 매칭 로직 — `FeedTitleFilter` + +외부 의존이 없는 순수 클래스로 분리한다(`domain/channel/FeedTitleFilter.java`). +`PersonPicks`, `VideoMetrics`와 같은 계열의 정적 유틸이다. + +```java +public static boolean matches(String filter, String title) +``` + +규칙: + +1. `filter`가 `null`/공백이면 **항상 `true`** (필터 없음). +2. `filter`를 콤마로 나눈다. 빈 조각은 버린다. 유효 키워드가 하나도 없으면 `true`. +3. 키워드와 제목 양쪽을 정규화한다: **소문자화 + 모든 공백류 제거**. +4. 정규화된 제목이 정규화된 키워드 중 **하나라도** 포함하면 `true` (OR). +5. `title`이 `null`/공백이면 `false` (필터가 있는 상태에서 제목을 못 읽으면 통과시키지 않는다). + +공백 제거가 핵심이다. 실제 유튜브 제목 표기가 `[유퀴즈 온 더 블럭]`, `유 퀴즈 온 더 블럭`, +`유퀴즈`로 흔들리는데, 공백을 지우면 키워드 `유퀴즈` 하나로 세 표기를 모두 잡는다. + +### 3. 수집 적용점 + +`ChannelService.upsertVideos`에는 이미 길이·기간 필터가 `continue`로 붙어 있다 +(피드 경로에서만 값이 들어오고 일반 채널 동기화는 `null`을 넘긴다). 같은 자리에 한 줄을 더한다. + +```java +if (minDurationSec != null && (durationSec == null || durationSec < minDurationSec)) continue; +if (publishedAfter != null && publishedAt.isBefore(publishedAfter)) continue; +if (!FeedTitleFilter.matches(titleFilter, title)) continue; // 추가 +``` + +- `upsertVideos` 시그니처에 `String titleFilter` 파라미터를 추가한다. +- `collectFeedVideos`가 `channel.getFeedTitleFilter()`를 꺼내 넘긴다. +- 일반 채널 동기화 호출부는 `null`을 넘겨 **영향받지 않는다**. + +**쿼터 영향 없음.** playlistItems 1페이지와 videos.list는 어차피 통째로 받고 +저장 단계에서만 거르는 구조라 채널당 2 units 그대로다. + +### 4. API + +| 메서드 | 경로 | 설명 | +|---|---|---| +| `POST` | `/api/feed/seeds` | 기존. body에 `titleFilter`(선택) 추가 | +| `PATCH` | `/api/feed/seeds/{id}/filter` | 신설. body `{"titleFilter": "유퀴즈"}` — 빈 값이면 필터 해제 | + +- `FeedService.addSeed(url, role, titleFilter)` — 기존 2-인자 오버로드는 `titleFilter=null`로 위임한다. +- `FeedService.updateSeedFilter(id, titleFilter)` — 피드 시드가 아니면 `IllegalArgumentException` + (`removeSeed`/`resetSeedFailure`와 동일한 방어). +- `FeedSeedDto`에 `titleFilter` 필드를 추가해 목록 조회에서 현재 값이 보이게 한다. + +### 5. UI (`templates/feed.html` 시드 관리 모달) + +- 시드 추가 영역: URL·역할 아래에 `제목 키워드 (선택, 콤마 구분)` 입력칸 추가. + 도움말: "비워두면 채널의 롱폼을 전부 수집합니다. 예: `유퀴즈` — 한 채널에 여러 프로그램이 섞여 있을 때 사용하세요." +- 시드 목록의 각 행: 필터가 있으면 채널명 아래 보조 줄에 `필터: 유퀴즈`를 표시한다. +- 각 행에 필터 수정 버튼(연필 아이콘)을 추가한다. 클릭 시 `prompt()`로 현재 값을 채워 띄우고, + 확인하면 `PATCH`를 호출한다. 기존 코드가 `confirm()`을 쓰고 있어 일관된다. + +### 6. 알려진 동작 + +필터를 **나중에** 건 경우, 이미 저장된 비매칭 영상은 삭제되지 않는다. 갱신 대상에서만 빠지고 +피드에는 계속 보인다. 재가공 텍스트·스크립트가 딸려 있을 수 있어 자동 삭제는 하지 않는다. +완전히 밀려면 시드를 해제(연결된 영상 함께 삭제)한 뒤 필터를 지정해 재등록한다. + +이 동작은 UI 도움말에 한 줄로 명시한다. + +## 테스트 + +현재 `src/test` 디렉터리가 없다. 이번에 만든다. + +`src/test/java/com/hlab/yanalyst/domain/channel/FeedTitleFilterTest.java` — 외부 API를 타지 않는 +순수 로직이라 테스트 가치가 가장 높은 지점이다. TDD로 이 클래스부터 작성한다. + +케이스: + +- 필터가 `null`/빈 문자열/콤마뿐이면 어떤 제목이든 통과 +- 공백 흔들림: 키워드 `유퀴즈` ↔ 제목 `[유 퀴즈 온 더 블럭] 123화` +- 대소문자 무시: 키워드 `showcase` ↔ 제목 `SHOWCASE` +- 콤마 OR: 키워드 `유퀴즈,핑계고` — 둘 중 하나만 걸려도 통과 +- 비매칭: 키워드 `유퀴즈` ↔ 제목 `놀면 뭐하니 190화` → `false` +- 제목이 `null`이고 필터가 있으면 `false` +- 키워드 조각의 앞뒤 공백은 무시 (`유퀴즈, 핑계고`) + +`build.gradle`에 테스트 의존성(`spring-boot-starter-test`)이 이미 있는지 확인하고, 없으면 추가한다. + +## 검증 + +```powershell +$env:JAVA_HOME = "D:\Development\app\JDK\jdk-21.0.5" +.\gradlew.bat test --tests "com.hlab.yanalyst.domain.channel.FeedTitleFilterTest" +.\gradlew.bat build +``` + +빌드 통과 후 앱을 띄워 `/feed` 시드 관리 모달에서 디글 클래식을 `유퀴즈` 필터로 등록하고 +수동 수집을 돌려 유퀴즈 영상만 들어오는지 확인한다.