쇼츠 클립 채널의 소재를 최신순으로 발굴하는 /feed 화면과 수집 파이프라인. - 소재 원본 탭: 웹예능 공식채널의 신규 롱폼(durationSec > 65)만 수집. 아직 아무도 안 자른 구간을 선점하는 용도라 공식계정 쇼츠는 제외한다. - 경쟁 쇼츠 탭: 예능짤 채널의 신규 쇼츠. 제목·썸네일 벤치마킹용이라 재가공 대신 원본 열기 액션을 준다. - Channel.role(MY/SOURCE/RIVAL) 컬럼 하나로 역할을 구분하고, 기존 uploads 플레이리스트 동기화를 그대로 재사용한다. 채널당 2 units라 search.list(100 units) 대비 쿼터가 거의 들지 않는다. - 3시간 주기 수집(FeedCollectionService). 소재 선점은 업로드 직후가 승부라 기존 일 1회 채널 수집으로는 늦다. - 수집함/발굴/떡상 후보 쿼리는 source 미지정 시 CHANNEL·SEARCH만 보도록 좁혀, 피드 영상이 기존 화면을 덮지 않게 격리했다. - 시드는 자동 후보 → 수동 승인. 내 채널 해시태그를 역분석해(HashtagExtractor) 공식채널 후보를 추천 목록에 쌓고, 승인 시 SOURCE/RIVAL로 등록한다. - 연속 3회 수집 실패한 시드는 자동 스킵해 쿼터 낭비를 막는다. - role이 null인 기존 채널은 부팅 시 MY로 1회 백필(ddl-auto:update 특성). UX: 기존 Editorial 디자인 시스템 유지. 골든타임(24h)·공식클립/풀에피· 떡상중·작업함 배지, 프로그램/길이/기간 필터, URL 상태 보존, 스켈레톤· 빈 상태·에러 복구 액션, 탭 키보드 이동, 이모지 대신 Lucide 아이콘. 테스트: HashtagExtractor·FeedBadges·ChannelRole 순수 로직 16건 추가(총 76건 통과). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
141 lines
7.3 KiB
Markdown
141 lines
7.3 KiB
Markdown
# 소재 발굴 피드 (Source Feed) 설계
|
||
|
||
작성일: 2026-07-31
|
||
|
||
## 배경
|
||
|
||
내 채널 `clipOut-log`(UCVJZ3_z7dqpUvaEgaWRltuA)는 한국 웹예능·토크쇼의 명장면을 잘라 올리는
|
||
쇼츠 전문 채널이다. 최근 50개 업로드 분석 결과 소재의 단위는 **"웹예능 에피소드 안의 한 순간"**이며,
|
||
반복 등장하는 소스는 유퀴즈·핑계고·미미미누·아는형님·살롱드립·짠한형 신동엽·요정식탁·어쩌다사장·
|
||
워크맨·입만열면·차린건쥐뿔도없지만이다.
|
||
|
||
기존 발굴(`/discover`, `/recommend`)은 **조회수순 떡상 채널 발굴**이라 목적이 다르다.
|
||
이 문서는 **"이런 소재를 최신순으로"** 를 위한 별도 피드를 정의한다.
|
||
|
||
## 목표
|
||
|
||
1. **선점** — 소스(웹예능 공식채널)의 신규 롱폼 업로드를 최신순으로 받아, 아직 아무도 안 자른 구간을 먼저 클립화
|
||
2. **추격** — 경쟁 예능짤 쇼츠 채널의 신규 쇼츠를 최신순으로 보며 반응 검증된 소재·제목·썸네일 벤치마킹
|
||
|
||
범위 밖(후속): 자막 기반 "터지는 구간" 자동 제안(2단계).
|
||
|
||
## 결정 사항
|
||
|
||
| 항목 | 결정 | 이유 |
|
||
|---|---|---|
|
||
| 탭 구성 | 소재 원본 / 경쟁 쇼츠 2탭 | 선점과 추격을 같이 보되 성격이 달라 액션이 다름 |
|
||
| 소스 탭 대상 | **롱폼만** (`durationSec > 65`) | 공식계정이 올린 쇼츠는 이미 잘린 결과물이라 소재 가치 없음 |
|
||
| 경쟁 탭 대상 | 쇼츠만 (`isShorts = true`) | 벤치마킹 대상 |
|
||
| 시드 확보 | 자동 후보 → 수동 승인(하이브리드) | 오탐 방지 + 자동 편재 |
|
||
| 피드 깊이 | 1단계(영상 목록)까지 | YAGNI. 구간 제안은 별도 과제 |
|
||
| 구현 방식 | 기존 채널 동기화 파이프라인 재사용 | 쿼터 60배 저렴, 재가공 스튜디오와 즉시 연결 |
|
||
|
||
## 아키텍처
|
||
|
||
### 데이터 모델
|
||
|
||
`Channel`에 역할 컬럼 하나만 추가한다.
|
||
|
||
```java
|
||
@Column(length = 10)
|
||
private String role = "MY"; // MY | SOURCE | RIVAL
|
||
```
|
||
|
||
`ddl-auto: update`는 기존 행을 NULL로 남기므로, 조회는 전부 `COALESCE(role,'MY')` 의미로 처리하고
|
||
앱 부팅 시 `role IS NULL → 'MY'` 1회 백필을 수행한다(프로젝트에 마이그레이션 파일이 없는 관례에 맞춤).
|
||
|
||
`ChannelVideo`는 컬럼 추가 없음. 기존 컬럼을 그대로 쓴다.
|
||
|
||
| 용도 | 기존 컬럼 |
|
||
|---|---|
|
||
| 피드 구분 | `source` — 기존 `CHANNEL`/`SEARCH` 에 `SOURCE`/`RIVAL` 값 추가 |
|
||
| 최신순 정렬 | `publishedAt` |
|
||
| 롱폼/쇼츠 구분 | `durationSec`, `isShorts` (`VideoMetrics.isShorts` = 65초 이하) |
|
||
| 경쟁 떡상 배지 | `viewsPerHour` |
|
||
| 이미 손댄 소재 | `interestStatus`, `bookmarked` |
|
||
| 시드 역분석 재료 | `hashtags` |
|
||
|
||
`RecommendedChannel`에 `roleHint`(SOURCE/RIVAL), `seedKeyword` 추가 — 승인 UI를 재사용하기 위함.
|
||
|
||
### 수집
|
||
|
||
`FeedCollectionService` 신설. 기존 `ChannelService`의 uploads 플레이리스트 동기화를 재사용한다.
|
||
|
||
- 주기 **3시간** (`hlab.feed.cron`). 소재 선점은 업로드 후 몇 시간이 승부라 일 1회로는 늦음
|
||
- 채널당 uploads **첫 페이지 50개**만 조회, `publishedAt`이 최근 N일(기본 14) 이내인 것만 upsert
|
||
- 소스 채널 → 롱폼만 저장(`source='SOURCE'`), 경쟁 채널 → 쇼츠만 저장(`source='RIVAL'`)
|
||
- `YoutubeQuotaGuard.tryConsume` 로 채널 단위 가드. 채널당 추정 2 units(playlistItems 1 + videos 1)
|
||
- 연속 3회 실패한 채널은 `feedFailCount >= 3` 로 자동 스킵(UI에서 초기화 가능)
|
||
|
||
기존 일일 채널 수집(`ScheduledCollectionService.runChannelCollection`)은 **role=MY 만** 대상으로
|
||
좁힌다. 백필 후 기존 채널은 전부 MY이므로 동작 변화 없음.
|
||
|
||
**수집함 격리** — `/collection`, `/discover`, 떡상 후보 쿼리는 `source` 를 명시하지 않은 경우
|
||
`CHANNEL`/`SEARCH` 만 대상으로 한다. 피드 영상이 기존 화면을 덮지 않는다.
|
||
|
||
### 화면 `/feed`
|
||
|
||
정렬은 `publishedAt DESC` 고정(정렬 셀렉터 없음).
|
||
|
||
**[소재 원본] 탭** — `source='SOURCE'`
|
||
- 카드: 썸네일 / 프로그램명 / 제목 / 상대시간 / 재생시간 / 조회수
|
||
- 배지: `골든타임`(24시간 이내), `공식클립`(65초~15분) / `풀에피`(15분+), `작업함`(interestStatus != NEW)
|
||
- 액션: 재가공(`/rework/{id}`), 숨김(EXCLUDED), 북마크
|
||
|
||
**[경쟁 쇼츠] 탭** — `source='RIVAL'`
|
||
- 배지: `떡상중`(viewsPerHour 상위), `골든타임`
|
||
- 액션: YouTube 열기, 북마크, 숨김 (남의 쇼츠는 재가공 대상이 아님)
|
||
|
||
공통 필터: 프로그램(채널) 선택 / 길이 / 기간(24h·3일·7일·14일) / 작업한 것 숨기기
|
||
|
||
### 시드 발굴
|
||
|
||
**소스 시드** — `SeedSuggestService`
|
||
1. `role=MY` 영상의 `hashtags`·제목에서 해시태그 빈도 집계(`HashtagExtractor`, 순수 로직)
|
||
2. 상위 키워드마다 `search.list type=channel&q=<키워드>` 조회 (100 units/건이라 **수동 실행 버튼**)
|
||
3. `RecommendedChannel(roleHint=SOURCE, status=NEW)` upsert → `/recommend`에서 승인
|
||
|
||
**경쟁 시드** — 기존 `ChannelDiscoveryService` 결과를 `roleHint=RIVAL` 로 승인 등록
|
||
|
||
**수동 등록** — `POST /api/feed/seeds {url, role}`
|
||
|
||
### API
|
||
|
||
| 메서드 | 경로 | 설명 |
|
||
|---|---|---|
|
||
| GET | `/api/feed` | `tab=SOURCE\|RIVAL`, `days`, `channelId`, `lengthBucket`, `hideWorked` |
|
||
| GET | `/api/feed/programs` | 필터용 채널 목록(피드에 영상이 있는 채널) |
|
||
| POST | `/api/feed/collect` | 수동 수집 1회 |
|
||
| GET | `/api/feed/seeds` | 등록된 시드 채널 목록 |
|
||
| POST | `/api/feed/seeds` | 수동 시드 등록 `{url, role}` |
|
||
| DELETE | `/api/feed/seeds/{id}` | 시드 해제(role → MY 로 되돌리지 않고 채널 삭제) |
|
||
| POST | `/api/feed/seeds/{id}/reset-failure` | 실패 카운터 초기화 |
|
||
| POST | `/api/feed/seeds/suggest` | 해시태그 역분석 → 소스 후보 발굴 |
|
||
|
||
### UX 원칙 (ui-ux-pro-max 적용)
|
||
|
||
기존 Editorial(almanac) 디자인 시스템을 그대로 따른다. 스킬이 제안한 OLED/핑크 팔레트는
|
||
**일관성 규칙(§4 consistency)에 따라 채택하지 않는다.** 적용하는 것은 규칙 쪽이다.
|
||
|
||
- 아이콘은 Lucide SVG만 사용(§4 no-emoji-icons) — 배지에 이모지 금지
|
||
- 터치 타겟 ≥44px, 카드 액션 간 8px 이상 간격(§2)
|
||
- 썸네일에 `aspect-ratio` + `width/height` 지정으로 CLS 방지, `loading="lazy"`(§3)
|
||
- 로딩은 스켈레톤(기존 `.skeleton` 재사용), 빈 상태·에러 상태 각각 안내 문구 + 복구 액션(§8)
|
||
- 애니메이션 150–300ms, `prefers-reduced-motion` 존중(§7)
|
||
- 탭은 `role="tablist"` + 키보드 좌우 이동, 현재 탭 `aria-selected`(§1, §9)
|
||
- 상태를 색으로만 전달하지 않음 — 배지에 텍스트 라벨 병기(§1 color-not-only)
|
||
- 필터 상태는 URL 쿼리에 반영해 새로고침·뒤로가기에서 보존(§9 state-preservation)
|
||
|
||
### 실패 처리
|
||
|
||
- 쿼터 소진 → 남은 채널 스킵, 요약에 `skippedByQuota` 보고
|
||
- 채널 uploads 플레이리스트 없음/삭제 → 로그 후 스킵, `feedFailCount++`
|
||
- 개별 영상 파싱 실패 → 해당 영상만 건너뜀
|
||
|
||
### 테스트
|
||
|
||
`src/test` 의 `DiscoveryRankerTest` 선례에 맞춰 **순수 로직만** 단위 테스트한다.
|
||
|
||
- `HashtagExtractorTest` — 해시태그 파싱·빈도 집계·불용어 제외
|
||
- `FeedBadgesTest` — 골든타임/길이버킷/떡상 판정 경계값
|