h-lab/docs/superpowers/specs/2026-07-31-source-feed-design.md
hehihoho3@gmail.com 592ed12c2b feat: 소재 발굴 피드 — 소스 롱폼/경쟁 쇼츠 최신순 2탭
쇼츠 클립 채널의 소재를 최신순으로 발굴하는 /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>
2026-07-31 15:42:26 +09:00

7.3 KiB
Raw Blame History

소재 발굴 피드 (Source Feed) 설계

작성일: 2026-07-31

배경

내 채널 clipOut-log(UCVJZ3_z7dqpUvaEgaWRltuA)는 한국 웹예능·토크쇼의 명장면을 잘라 올리는 쇼츠 전문 채널이다. 최근 50개 업로드 분석 결과 소재의 단위는 **"웹예능 에피소드 안의 한 순간"**이며, 반복 등장하는 소스는 유퀴즈·핑계고·미미미누·아는형님·살롱드립·짠한형 신동엽·요정식탁·어쩌다사장· 워크맨·입만열면·차린건쥐뿔도없지만이다.

기존 발굴(/discover, /recommend)은 조회수순 떡상 채널 발굴이라 목적이 다르다. 이 문서는 "이런 소재를 최신순으로" 를 위한 별도 피드를 정의한다.

목표

  1. 선점 — 소스(웹예능 공식채널)의 신규 롱폼 업로드를 최신순으로 받아, 아직 아무도 안 자른 구간을 먼저 클립화
  2. 추격 — 경쟁 예능짤 쇼츠 채널의 신규 쇼츠를 최신순으로 보며 반응 검증된 소재·제목·썸네일 벤치마킹

범위 밖(후속): 자막 기반 "터지는 구간" 자동 제안(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/SEARCHSOURCE/RIVAL 값 추가
최신순 정렬 publishedAt
롱폼/쇼츠 구분 durationSec, isShorts (VideoMetrics.isShorts = 65초 이하)
경쟁 떡상 배지 viewsPerHour
이미 손댄 소재 interestStatus, bookmarked
시드 역분석 재료 hashtags

RecommendedChannelroleHint(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)
  • 애니메이션 150300ms, prefers-reduced-motion 존중(§7)
  • 탭은 role="tablist" + 키보드 좌우 이동, 현재 탭 aria-selected(§1, §9)
  • 상태를 색으로만 전달하지 않음 — 배지에 텍스트 라벨 병기(§1 color-not-only)
  • 필터 상태는 URL 쿼리에 반영해 새로고침·뒤로가기에서 보존(§9 state-preservation)

실패 처리

  • 쿼터 소진 → 남은 채널 스킵, 요약에 skippedByQuota 보고
  • 채널 uploads 플레이리스트 없음/삭제 → 로그 후 스킵, feedFailCount++
  • 개별 영상 파싱 실패 → 해당 영상만 건너뜀

테스트

src/testDiscoveryRankerTest 선례에 맞춰 순수 로직만 단위 테스트한다.

  • HashtagExtractorTest — 해시태그 파싱·빈도 집계·불용어 제외
  • FeedBadgesTest — 골든타임/길이버킷/떡상 판정 경계값