docs: 시드 채널 제목 키워드 필터 설계

디글 클래식처럼 한 채널에 여러 프로그램이 섞인 경우 특정 프로그램만
피드에 수집하기 위한 설계. Channel에 feed_title_filter 컬럼을 추가하고
공백 무시 부분일치 OR 매칭으로 수집 단계에서 거른다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-11 12:38:07 +09:00
parent 7d8c6bc49b
commit b8b119a94a

View File

@ -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` 시드 관리 모달에서 디글 클래식을 `유퀴즈` 필터로 등록하고
수동 수집을 돌려 유퀴즈 영상만 들어오는지 확인한다.