수동 작업 안내서

코드 반영 후 사람이 해야 하는 것검증 방법 · 2026-07-30 갱신 · 관련: REFINEMENT-WORKFLOW.md §TODO · 적합성 조사
진행 현황 — fetcher 계층은 사실상 완료

완료 — sourceType 25종 분리 · 라우터 7소셜 · 6개 소셜 전부 조인키 캡처 · YouTube Data API v3 전환 · Reddit Apify 전환 · 테스트 164/164

남은 수동 작업 — instagram actor 비교(선택) 뿐. 나머지는 스키마 구조 결정(Tier 1) 대기.

#작업상태결과
1YouTube Data API v3 키 발급완료 키 등록됨(.env.dev/.env.prod). 실호출 4/4 성공 — 모든 채널 경로가 canonical channelId 로 수렴
2X fetcher 실호출 확인완료 quote 트윗 1콜로 검증. 중첩 인용의 필드 누락을 발견해 수정(아래 §완료 상세)
3reddit Apify actor 검증완료 body·crawledAt 실재 확인 → Apify 전환 완료. 실캡처 0 상태 해소
4instagram prodiger drop-in 검증남음 actor 교체 여부. 월 $3 차이라 급하지 않음 — 매핑 확장은 현행 actor 로 이미 완료
5라우터 회귀 확인완료 router-cases 82건 · 전체 164/164

완료 상세 검증 과정에서 드러난 것

소셜확인된 것
youtube oEmbed 는 channelId 를 안 줘 조인이 성립하지 않았다 → Data API v3 전환. 실측: watch?v=dQw4w9WgXcQUCuAXFkgsw1L7xaCfnd5JJOw · @nateherkUC2ojq-nuP8ceeHqiroeKhBA · /user/PewDiePieUC-lHJZR3Gqxm24_Vd_AJ5Yw. favoriteCount 가 조회수 18억 영상도 "0" 이라 audit 의 drop 판정이 실측 확인됨. 채널 경로마다 파라미터가 갈려(id/forHandle/forUsername) 폴백 추가
x 인용 원본의 본문이 "https://t.co/g7TBcmfv8I" 처럼 축약링크 하나뿐인 케이스 발견 — 중첩에 urls 가 없어 링크 정체를 알 수 없었다. tweetData/nestedTweetData 를 한 함수로 통일(깊이 2 제한)해 최상위와 필드 집합을 일치시킴. 수정 후 실측: quoted.urls = ["http://x.com/i/article/2080704598503772160"]
reddit about.json 이 IP 전면 차단(403)으로 실캡처 0 → Apify 전환. rubric 11b 가 적발한 body·crawledAt 실재 확인. actor 가 ageHours·scorePerHour 등 파생값을 주지만 crawledAt 기준이라 저장 금지 (audit 판정이 실측으로 정당화). subreddit URL 이 dataType='post' 를 뱉어 Venue 자체 정보는 못 얻음(알려진 한계)
tiktok · instagram · github 조인키(authorMeta.id · ownerId · owner.id)와 audit keep 필드 반영. github 은 무료라 실호출 4/4 검증 — id=1166408297, ownerId=48523873

1. YouTube Data API v3 키 발급 완료 — 절차는 재발급 시 참고용

왜 필요한가

현재 youtube.fetcher.ts무료 oEmbed 만 씁니다. oEmbed 응답에는 title·author_name·author_url·thumbnail_url 넷뿐이고 channelId 가 없습니다.

audit 이 확정한 YouTube 조인키가 channelId(UCxxx)인데, 지금 코드로는 author_url@handle 밖에 못 얻습니다. handle 은 변경 가능하고 옛 핸들과 분기되므로 조인이 성립하지 않습니다. 게다가 구독자수·조회수·게시일도 전부 못 받습니다.

발급 절차

  1. Google Cloud Console 접속 → console.cloud.google.com
  2. 프로젝트 생성 (또는 기존 프로젝트 선택). 상단 프로젝트 선택기 → "새 프로젝트"
  3. YouTube Data API v3 사용 설정
    좌측 메뉴 API 및 서비스라이브러리 → "YouTube Data API v3" 검색 → 사용 클릭
  4. API 키 생성
    API 및 서비스사용자 인증 정보+ 사용자 인증 정보 만들기API 키
  5. 키 제한(권장)
    생성된 키 → API 제한사항 → "키 제한" 선택 → YouTube Data API v3만 체크. 애플리케이션 제한은 서버에서 쓰므로 "없음" 또는 IP 제한.
  6. .env.dev 에 추가
    YOUTUBE_API_KEY=AIza...
    .env.prod 에도 동일하게 추가해야 프로덕션에서 동작합니다.
할당량 — 무료로 충분합니다
항목
일일 무료 할당량10,000 units
videos.list 1회1 unit
channels.list 1회1 unit
실측 관측량youtube URL 514건(전체 샘플 기준)

→ 하루 10,000회 조회 가능한데 관측량이 514건이라 여유가 20배 이상입니다. 과금 설정 불필요.

키 발급 후 즉시 확인 (무료)

구현 전에 키가 살아있는지, 그리고 정말 channelId 가 오는지 먼저 확인하세요.

# 1) 영상 조회 — snippet.channelId 가 오는지가 핵심
export YT_KEY='여기에_발급받은_키'

curl -s "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&id=dQw4w9WgXcQ&key=$YT_KEY" \
  | python3 -m json.tool | head -40

# 기대: items[0].snippet.channelId = "UCuAXFkgsw1L7xaCfnd5JJOw"
#       items[0].statistics.viewCount / likeCount / commentCount

# 2) 핸들 → channelId 해석 (audit 이 "handle 로 channel id 를 찾을 수 있나" 물었던 부분)
curl -s "https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&forHandle=@nateherk&key=$YT_KEY" \
  | python3 -m json.tool | head -40

# 기대: items[0].id = "UC..." · snippet.publishedAt(계정 개설일) · statistics.subscriberCount
이 두 응답을 저에게 주세요

audit 의 YouTube objectFields 는 라이브 검증을 한 번도 못 한 상태(키가 없어서)라 API 문서 기반 추정입니다. 실제 응답을 보면 필드 목록을 확정하고 fetcher 를 정확히 구현할 수 있습니다.

응답에 개인정보는 없습니다(공개 채널·영상 메타데이터). 키 값만 가려서 주시면 됩니다.

2. 수정한 fetcher 검증 방법 공통

방법 A — HTTP API (권장, 가장 간단)

디버깅 전용 엔드포인트가 이미 있습니다. URL 1건을 라우팅+fetch 해서 결과를 그대로 돌려줍니다.

# 서버 기동 (포트는 .env.dev 의 APP_PORT=30003)
npm run start:dev

# 다른 터미널에서
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://x.com/solana' \
  | python3 -m json.tool
쿼리 파라미터의미기본값
url분류·fetch 대상 URL (필수)
enablePaidApify 유료 게이트(tiktok·instagram). falseskipped_paidfalse
enableCommunityX 커뮤니티 fetch (20 credits/호출, 느림)false
urlscanModesearch(가장 이른 기록) / scan(현재 렌더)search
응답에서 볼 곳
{
  "sourceType": "x_tweet",      ← 분류가 맞나
  "sourceKey":  "2070...",      ← 키가 맞나(리터럴 'playlist' 같은 게 아닌지)
  "status":     "ok",
  "valueNature": { ... },        ← fixed/mutable 표기
  "data": { ... }                ← 실제 담긴 필드
}

방법 B — 라우터만 확인 (서버·API 불필요, 무료)

fetch 없이 분류만 보고 싶을 때. 네트워크를 전혀 안 탑니다.

npx jest test/social-fetcher/router-cases.test.ts

# 새 URL 을 케이스로 추가하려면 test/social-fetcher/router-cases.json 에 한 줄:
# { "url": "...", "sourceType": "...", "sourceKey": "...", "group": "manual" }

3. 소셜별 검증 1·2·3 완료 · 4 남음

3-1. X 완료 유료 1콜

# 인용(quote) 트윗으로 테스트해야 nested 필드까지 검증됨
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://x.com/jncquant/status/2070140594496626759' \
  | python3 -m json.tool
확인할 것기대
data.id"2070140594496626759"콘텐츠 dedup 키 — 이전엔 아예 없었음
data.authorId숫자 문자열creator 조인키 — 이전엔 handle 만 있었음
data.quoted.id숫자 문자열가장 중요 — 인용 원본을 1급 레코드로 승격하는 전제
data.quoted.authorId숫자 문자열인용 작성자 조인키
data.urls배열(t.co 원본 URL)드레이너 도메인·CA 추적
data.quoteCount·bookmarkCount숫자audit 이 keep 지정
valueNature.authorUserName"mutable"이전엔 fixed 로 look-ahead 안전성을 잘못 보증
정지 계정 케이스도 같이 (유료 1콜 추가)
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://x.com/nvidiamoonbsc' \
  | python3 -m json.tool

기대: status: "ok" + data.unavailable: true + data.unavailableReason: "Suspended"

이전엔 createdAt(undefined).toISOString() 에서 TypeError 가 나 status:"error" 로 뭉개졌습니다. 해당 계정이 복구됐을 수도 있으니 정지 계정이 아니면 그냥 정상 응답이 옵니다(그것도 정상).

3-2. 라우터 분류 무료 — 이번에 고친 버그 3건

fetch 를 안 타므로 비용 0입니다. sourceType·sourceKey 만 보면 됩니다.

# ① reddit share — 이전엔 reddit/{subreddit} 로 오분류(share 가 subreddit 과 한 키로 병합)
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://www.reddit.com/r/elonmusk/s/SZ4HmKd53H'
# 기대: sourceType=reddit_share, sourceKey=SZ4HmKd53H

# ② youtube playlist — 이전엔 모든 재생목록이 sourceKey='playlist' 를 공유
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://www.youtube.com/playlist?list=PLabc123'
# 기대: sourceType=youtube_playlist, sourceKey=PLabc123 (status=unsupported — fetch 보류가 의도)

# ③ instagram /{handle}/reel/{code} — 이전엔 handle 을 키로 잡아 게시물이 프로필로 오분류
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://www.instagram.com/someuser/reel/DZvu_BVzM5W/'
# 기대: sourceType=instagram_post, sourceKey=DZvu_BVzM5W

# ④ vm.tiktok.com — 이전엔 unknown 이라 통째 유실
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://vm.tiktok.com/ZNRTtUjSM/'
# 기대: sourceType=tiktok_shortlink, sourceKey=ZNRTtUjSM (status=unsupported — 상류 정규화 예정)

# ⑤ github 예약경로 — 이전엔 sponsors/xxx 를 repo 로 오인
curl -s 'http://localhost:30003/v1/social-fetcher/fetch?url=https://github.com/sponsors/someone'
# 기대: sourceType=unknown
unsupported 는 정상입니다

youtube_playlist·tiktok_shortlink·reddit_share·instagram_story·github_gist의도적으로 fetcher 에 미등록했습니다. 분류는 되지만 fetch 는 안 하는 상태이며, 이전의 unknown(=키조차 없음)보다 개선된 것입니다.

3-3. reddit Apify 검증 완료

현재 reddit 은 실캡처 0 입니다

reddit.fetcher.tsabout.json 을 쓰는데 Reddit 이 우리 IP 를 전면 차단합니다(전 경로 403 실측). audit 은 Apify harshmaur~reddit-scraper 로 전환하기로 확정했지만 라이브 검증을 한 적이 없습니다.

# APIFY_TOKEN 은 .env.dev 에 이미 있음 (값 출력 금지)
export APIFY_TOKEN=$(grep '^APIFY_TOKEN=' .env.dev | cut -d= -f2-)

curl -s -X POST \
  "https://api.apify.com/v2/acts/harshmaur~reddit-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN&timeout=120" \
  -H 'Content-Type: application/json' \
  -d '{"startUrls":[{"url":"https://www.reddit.com/r/wallstreetbets/"}],"maxItems":1}' \
  | python3 -m json.tool | head -60

확인할 것: 응답 필드가 문서의 raw 부록과 일치하는지 — 특히 body/bodyHtml(본문) · crawledAt(as-of 앵커) · parsedId · authorName. 이 둘은 완료 rubric 이 "문서에서 통째로 빠져 있었다"고 적발한 필드라 실재 여부 확인이 필요합니다.

3-4. instagram prodiger drop-in 검증 남음 유료 2콜 · 급하지 않음

actor 교체(현행 apify~instagram-scraperprodiger/instagram-scraper)는 월 $3 차이라 우선순위가 낮습니다. 응답 스키마가 동일한 drop-in 인지만 확인하면 됩니다.

# 같은 URL 을 두 actor 로 호출해 키 집합 비교
for ACTOR in "apify~instagram-scraper" "prodiger~instagram-scraper"; do
  echo "--- $ACTOR"
  curl -s -X POST "https://api.apify.com/v2/acts/$ACTOR/run-sync-get-dataset-items?token=$APIFY_TOKEN&timeout=120" \
    -H 'Content-Type: application/json' \
    -d '{"directUrls":["https://www.instagram.com/knowyourmeme/"],"resultsType":"details","resultsLimit":1}' \
    | python3 -c "import json,sys; d=json.load(sys.stdin); print(sorted(d[0].keys()) if d else 'EMPTY')"
done

키 집합이 같으면 drop-in. 다르면 현행 유지(비용차가 무의미).

4. 전체 회귀 확인 무료 · 언제든

# social-fetcher 전체 (139개)
npx jest test/social-fetcher

# 타입 체크 — social/twitter 관련만 필터(레포에 무관한 사전 에러가 다수 있음)
npx tsc --noEmit -p tsconfig.json 2>&1 | grep -icE 'social|twitter'
# 기대: 0

5. 남은 것 fetcher 계층 밖

항목내용블록
Tier 1 구조 4건 content_raw 분리 · 상태 SoT 확정 · sourceType→컬렉션 매핑 · platform 키 충돌 — 모델 신설의 전제 사용자 결정
instagram actor 교체prodiger drop-in 여부(§3-4). 월 $3 차이유료 2콜
상류 canonical 정규화TikTok shortlink 를 저장 전 HEAD 301 로 펼침. 상류 파이프라인 위치 확인 필요범위 확인
Venue(subreddit) 조달Apify 가 subreddit URL 에 post 를 뱉어 개설일·description 미확보입력 옵션 탐색
reddit_post·reddit_user 경로subreddit 리스팅만 실측 — 나머지 두 경로 미검증유료 2콜

주의