← 목록으로
2026-02-26plans

title: SNS 자동 수집 파이프라인 (YouTube RSS + SNS) date: 2026-02-26 type: implementation-plan project: content-pipeline status: draft task_id: 947dc9b8-bd96-4134-a92f-f18be0327bfb repo: migkjy/ai-blog

SNS 자동 수집 파이프라인 구현 플랜

1. 배경 및 목적

현재 content-pipeline은 RSS 피드(17개 소스)에서만 AI 뉴스를 수집한다. CEO가 소비하는 YouTube 채널, Threads/Instagram 트렌드 등은 수동으로 확인해야 하므로 콘텐츠 커버리지에 공백이 생긴다.

목표: RSS 이외의 추가 소스(YouTube, Threads, Instagram)를 자동 수집하여 content-pipeline input을 확장한다.

2. 현재 아키텍처 분석

2.1 기존 수집 패턴 (src/pipeline/collect.ts)

FeedSource[] (17개 피드 목록)
  → rss-parser로 파싱 (10초 타임아웃, 피드당 최대 10건)
  → CollectedItem 인터페이스로 정규화
  → URL 정규화 + 제목 유사도 기반 중복 제거
  → saveCollectedNews()로 DB 저장 (ON CONFLICT DO NOTHING)

핵심 인터페이스:

interface FeedSource {
  name: string;
  url: string;
  lang: "en" | "ko";
  grade: "S" | "A" | "B";
  category: "news" | "official" | "community" | "research";
}

interface CollectedItem {
  title: string;
  url: string;
  source: string;
  lang: string;
  grade: string;
  category: string;
  summary: string | null;
  content_snippet: string | null;
  published_at: Date | null;
}

2.2 파이프라인 스테이지 구조

stage-collect.ts  → collectNews() 호출 + pipeline_logs 기록
stage-generate.ts → AI 콘텐츠 생성
stage-publish.ts  → 승인 후 발행

Cron: GET /api/cron/pipeline (매일 06:00 KST, 월~금)

  • Stage 1(수집) → Stage 2(생성) 순차 실행

2.3 DB 스키마 (collected_news 테이블)

collected_news (
  id, title, url UNIQUE, source, summary, content_snippet,
  published_at, used_in_newsletter, created_at
)

2.4 2단계 필터링 프로세스 (rss-filter-process.md)

1단계: 경량 필터링 (키워드 매칭, 등급별 우선순위, 토큰 0) 2단계: AI 요약 생성 (1단계 통과분만)

새 수집 소스도 이 2단계 필터를 동일하게 통과시켜야 한다.

3. 구현 범위 및 우선순위

우선순위 기준: CEO 블로킹 없는 것 우선

순위소스API 키 필요CEO 블로킹구현 난이도
1YouTube 채널 RSS없음없음낮음
2YouTube 플레이리스트 RSS없음없음낮음
3YouTube Data API (좋아요/저장)YOUTUBE_API_KEY있음중간
4Threads/Instagram공식 API 제한있음높음 (mock)

4. 태스크 분할


Task 1: YouTube RSS 수집기 구현 (collect-youtube.ts)

목표: YouTube 채널/플레이리스트 RSS를 기존 collect.ts 패턴과 동일하게 수집

구현 상세:

1-1. YouTube RSS 소스 목록 (YOUTUBE_FEEDS)

export interface YouTubeFeedSource {
  name: string;
  channelId?: string;      // 채널 RSS용
  playlistId?: string;     // 플레이리스트 RSS용
  lang: "en" | "ko";
  grade: "S" | "A" | "B";
  category: "tutorial" | "news" | "review" | "talk";
}

YouTube RSS URL 형식:

  • 채널: https://www.youtube.com/feeds/videos.xml?channel_id={CHANNEL_ID}
  • 플레이리스트: https://www.youtube.com/feeds/videos.xml?playlist_id={PLAYLIST_ID}

초기 채널 목록 (AI 관련 한/영 채널 5~8개):

  • AI 관련 한국 채널: 노마드코더, 조코딩 등 (CEO 구독 확인 후 확정)
  • 해외: Fireship, Two Minute Papers, AI Explained 등

1-2. collectYouTube() 함수

export async function collectYouTube(): Promise<CollectedItem[]> {
  // 기존 rss-parser 재활용 (YouTube RSS = Atom feed, rss-parser 지원)
  // 피드당 최대 5건 (영상은 RSS보다 빈도 낮음)
  // CollectedItem으로 정규화:
  //   - title: 영상 제목
  //   - url: 영상 URL (https://www.youtube.com/watch?v=XXX)
  //   - source: 채널명
  //   - category: youtube_feed.category
  //   - summary: 영상 description 첫 500자
  //   - content_snippet: null (영상은 텍스트 본문 없음)
  //   - published_at: pubDate
}

1-3. 기존 collectNews()에 통합 OR 별도 호출

방안 A (추천): stage-collect.ts에서 collectNews() + collectYouTube() 병렬 호출 후 합산

  • 기존 collect.ts 코드 변경 최소화
  • YouTube 수집 실패해도 RSS 수집에 영향 없음
// stage-collect.ts 수정
const [rssItems, ytItems] = await Promise.all([
  collectNews(),
  collectYouTube(),
]);
const allItems = [...rssItems, ...ytItems];
const saved = await saveCollectedNews(allItems);

방안 B: collect.tsRSS_FEEDS에 YouTube RSS URL 직접 추가

  • 가장 간단하지만 YouTube 특유의 메타데이터(channelId 등) 활용 불가
  • YouTube RSS 실패 시 일반 RSS와 동일하게 처리됨

결정: 방안 A -- 모듈 분리가 향후 YouTube API 통합 시에도 유리

1-4. collected_news 테이블 활용

기존 collected_news 테이블을 그대로 사용한다:

  • source 컬럼에 "YouTube: {채널명}" 형태로 저장
  • url UNIQUE 제약으로 중복 영상 자동 방지
  • 새 컬럼 추가 불필요 (MVP 원칙)

산출물: src/pipeline/collect-youtube.ts 예상 코드량: ~120줄 CEO 블로킹: 없음


Task 2: stage-collect.ts 통합 및 Cron 파이프라인 수정

목표: YouTube 수집을 기존 파이프라인 스테이지에 통합

구현 상세:

2-1. stage-collect.ts 수정

import { collectNews, saveCollectedNews } from './collect';
import { collectYouTube } from './collect-youtube';

export async function runCollectStage(triggerType): Promise<CollectResult> {
  // ... 기존 pipeline_logs 기록 로직 유지

  // 병렬 수집
  const [rssItems, ytItems] = await Promise.allSettled([
    collectNews(),
    collectYouTube(),
  ]);

  const allItems = [
    ...(rssItems.status === 'fulfilled' ? rssItems.value : []),
    ...(ytItems.status === 'fulfilled' ? ytItems.value : []),
  ];

  const saved = await saveCollectedNews(allItems);
  // ... 기존 로깅/통계 로직

  // metadata에 youtube 통계 추가
  const metadata = {
    rss_items: rssItems.status === 'fulfilled' ? rssItems.value.length : 0,
    youtube_items: ytItems.status === 'fulfilled' ? ytItems.value.length : 0,
    saved_items: saved,
  };
}

2-2. Cron 엔드포인트는 변경 불필요

/api/cron/pipelinerunCollectStage()만 호출하므로, stage-collect 수정만으로 자동 반영.

2-3. CLI 스크립트 추가

// package.json scripts
"pipeline:collect:youtube": "tsx src/pipeline/collect-youtube.ts"

산출물: stage-collect.ts 수정, package.json 스크립트 추가 예상 코드량: ~30줄 수정 CEO 블로킹: 없음


Task 3: YouTube 채널 목록 설정 파일

목표: CEO가 쉽게 채널을 추가/제거할 수 있도록 설정 분리

구현 상세:

3-1. src/config/youtube-channels.ts 생성

import { YouTubeFeedSource } from '../pipeline/collect-youtube';

export const YOUTUBE_CHANNELS: YouTubeFeedSource[] = [
  // === AI/Tech 한국어 채널 ===
  {
    name: "노마드코더",
    channelId: "UCUpJs89fSBXNolQGOYKn0YQ",
    lang: "ko",
    grade: "A",
    category: "tutorial",
  },
  {
    name: "조코딩",
    channelId: "UCQNE2JmbasNYbjGAcuBiRRg",
    lang: "ko",
    grade: "A",
    category: "tutorial",
  },
  // === AI/Tech 영어 채널 ===
  {
    name: "Fireship",
    channelId: "UCsBjURrPoezykLs9EqgamOA",
    lang: "en",
    grade: "S",
    category: "news",
  },
  {
    name: "Two Minute Papers",
    channelId: "UCbfYPyITQ-7l4upoX8nvctg",
    lang: "en",
    grade: "A",
    category: "review",
  },
  {
    name: "AI Explained",
    channelId: "UCNJ1Ymd5yFuUPtn21xtRbbw",
    lang: "en",
    grade: "S",
    category: "news",
  },
  {
    name: "Matt Wolfe",
    channelId: "UCJIBYMRMEfqVMGJJgWVAmnw",
    lang: "en",
    grade: "A",
    category: "review",
  },
];

3-2. CEO가 채널 추가하는 방법

해당 파일에 객체 추가만 하면 됨. channelId는 YouTube 채널 페이지 소스에서 확인 가능. 향후 UI 대시보드에서 관리 가능 (Phase 2).

산출물: src/config/youtube-channels.ts 예상 코드량: ~60줄 CEO 블로킹: 채널 목록 확정 (확정 전까지 기본 목록으로 동작)


Task 4: Threads/Instagram Mock 인터페이스 + 향후 확장점

목표: SNS 수집 확장 포인트만 마련 (실제 구현은 API 확보 후)

구현 상세:

4-1. 현실적 대안 분석

플랫폼공식 API대안실현 가능성
Threads없음 (Meta Basic API만)해시태그 RSS 서비스, 웹 크롤링낮음 (ToS 위반 위험)
InstagramGraph API (비즈니스만)해시태그 RSS, 공개 프로필중간 (인증 복잡)
X/TwitterAPI v2 (유료)RSS 서비스 (Nitter 등 불안정)중간 (비용 발생)

4-2. collect-sns.ts Mock 인터페이스

export interface SnsFeedSource {
  name: string;
  platform: "threads" | "instagram" | "twitter";
  identifier: string;    // 계정 핸들 또는 해시태그
  lang: "en" | "ko";
  grade: "S" | "A" | "B";
  category: string;
}

export async function collectSns(): Promise<CollectedItem[]> {
  // Phase 1: Mock 반환 (빈 배열)
  // Phase 2: 실제 API/크롤링 구현 시 채워넣기
  console.log('[collect-sns] SNS 수집은 Phase 2에서 구현 예정');
  return [];
}

4-3. stage-collect.ts에 미리 연결

const [rssItems, ytItems, snsItems] = await Promise.allSettled([
  collectNews(),
  collectYouTube(),
  collectSns(),    // Phase 1: 빈 배열 반환
]);

이렇게 하면 향후 SNS API가 확보되었을 때 collectSns() 내부만 채우면 된다.

산출물: src/pipeline/collect-sns.ts (mock) 예상 코드량: ~40줄 CEO 블로킹: Threads/Instagram API 접근 방법 결정 (Phase 2)


Task 5: 테스트 및 검증

목표: YouTube RSS 수집이 정상 동작하는지 검증

구현 상세:

5-1. CLI 단독 실행 테스트

# YouTube 수집만 단독 실행
npx tsx src/pipeline/collect-youtube.ts

# 기대 출력:
# [collect-youtube] Fetching: Fireship ...
# [collect-youtube]   Fireship: 5 items
# [collect-youtube] Summary: 6/6 channels OK, 0 failed, 28 raw → 25 after dedup

5-2. 통합 파이프라인 테스트

# stage-collect 전체 실행 (RSS + YouTube)
npx tsx -e "import { runCollectStage } from './src/pipeline/stage-collect'; runCollectStage('manual').then(r => console.log(JSON.stringify(r, null, 2)))"

5-3. 검증 항목

  • YouTube RSS 파싱 정상 (rss-parser가 Atom feed 처리)
  • CollectedItem 정규화 정상 (title, url, source 등)
  • 중복 제거 정상 (deduplicateNews 재활용)
  • DB 저장 정상 (collected_news에 youtube 소스 포함)
  • 기존 RSS 수집에 영향 없음 (Promise.allSettled 격리)
  • YouTube 피드 실패 시 RSS만으로 정상 동작

산출물: 테스트 실행 로그 CEO 블로킹: 없음


5. 파일 변경 요약

파일변경 유형설명
src/pipeline/collect-youtube.ts신규YouTube RSS 수집기
src/config/youtube-channels.ts신규YouTube 채널 목록
src/pipeline/collect-sns.ts신규SNS 수집 mock 인터페이스
src/pipeline/stage-collect.ts수정YouTube + SNS 수집 통합
package.json수정CLI 스크립트 추가

기존 코드 변경 최소화 원칙 준수: collect.ts는 변경하지 않음.

6. CEO 블로킹 항목

항목블로킹 대상대안
YouTube 채널 목록 확정Task 3 (설정)기본 AI 채널 6개로 시작 가능
YOUTUBE_API_KEYTask 없음 (이번 범위 밖)RSS로 대체, API 불필요
Threads/Instagram APITask 4 (mock만)Phase 2로 이관

핵심: Task 1~3은 CEO 블로킹 없이 즉시 구현 가능

7. 의존성

  • 새로운 npm 패키지 없음 (기존 rss-parser 재활용)
  • DB 스키마 변경 없음 (기존 collected_news 테이블 그대로 사용)
  • 환경변수 추가 없음

8. 리스크 및 완화

리스크확률완화
YouTube RSS 응답 지연/차단낮음10초 타임아웃 + Promise.allSettled 격리
rss-parser가 YouTube Atom feed 미지원매우 낮음rss-parser v3.13은 Atom 지원 확인됨
수집량 증가로 토큰 낭비중간rss-filter-process 2단계 필터를 YouTube에도 동일 적용
YouTube 채널 ID 오류낮음수집 시 개별 피드 실패는 skip 처리

9. 예상 효과

  • 콘텐츠 소스 17개 → 2325개로 확장 (YouTube 68개 추가)
  • YouTube AI 콘텐츠 트렌드를 블로그/뉴스레터에 반영 가능
  • CEO 블로킹 없이 즉시 실행 가능한 인프라 확장
  • 향후 SNS 소스 추가 시 collect-sns.ts 내부만 구현하면 됨
plans/2026/02/26/sns-collect-pipeline.md