쇼츠 클립 채널의 소재를 최신순으로 발굴하는 /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>
7.3 KiB
소재 발굴 피드 (Source Feed) 설계
작성일: 2026-07-31
배경
내 채널 clipOut-log(UCVJZ3_z7dqpUvaEgaWRltuA)는 한국 웹예능·토크쇼의 명장면을 잘라 올리는
쇼츠 전문 채널이다. 최근 50개 업로드 분석 결과 소재의 단위는 **"웹예능 에피소드 안의 한 순간"**이며,
반복 등장하는 소스는 유퀴즈·핑계고·미미미누·아는형님·살롱드립·짠한형 신동엽·요정식탁·어쩌다사장·
워크맨·입만열면·차린건쥐뿔도없지만이다.
기존 발굴(/discover, /recommend)은 조회수순 떡상 채널 발굴이라 목적이 다르다.
이 문서는 "이런 소재를 최신순으로" 를 위한 별도 피드를 정의한다.
목표
- 선점 — 소스(웹예능 공식채널)의 신규 롱폼 업로드를 최신순으로 받아, 아직 아무도 안 자른 구간을 먼저 클립화
- 추격 — 경쟁 예능짤 쇼츠 채널의 신규 쇼츠를 최신순으로 보며 반응 검증된 소재·제목·썸네일 벤치마킹
범위 밖(후속): 자막 기반 "터지는 구간" 자동 제안(2단계).
결정 사항
| 항목 | 결정 | 이유 |
|---|---|---|
| 탭 구성 | 소재 원본 / 경쟁 쇼츠 2탭 | 선점과 추격을 같이 보되 성격이 달라 액션이 다름 |
| 소스 탭 대상 | 롱폼만 (durationSec > 65) |
공식계정이 올린 쇼츠는 이미 잘린 결과물이라 소재 가치 없음 |
| 경쟁 탭 대상 | 쇼츠만 (isShorts = true) |
벤치마킹 대상 |
| 시드 확보 | 자동 후보 → 수동 승인(하이브리드) | 오탐 방지 + 자동 편재 |
| 피드 깊이 | 1단계(영상 목록)까지 | YAGNI. 구간 제안은 별도 과제 |
| 구현 방식 | 기존 채널 동기화 파이프라인 재사용 | 쿼터 60배 저렴, 재가공 스튜디오와 즉시 연결 |
아키텍처
데이터 모델
Channel에 역할 컬럼 하나만 추가한다.
@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
role=MY영상의hashtags·제목에서 해시태그 빈도 집계(HashtagExtractor, 순수 로직)- 상위 키워드마다
search.list type=channel&q=<키워드>조회 (100 units/건이라 수동 실행 버튼) 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— 골든타임/길이버킷/떡상 판정 경계값