완료 — sourceType 25종 분리 · 라우터 7소셜 · 6개 소셜 전부 조인키 캡처 · YouTube Data API v3 전환 · Reddit Apify 전환 · 테스트 164/164
남은 수동 작업 — instagram actor 비교(선택) 뿐. 나머지는 스키마 구조 결정(Tier 1) 대기.
| # | 작업 | 상태 | 결과 |
|---|---|---|---|
| 1 | YouTube Data API v3 키 발급 | 완료 | 키 등록됨(.env.dev/.env.prod). 실호출 4/4 성공 — 모든 채널 경로가 canonical channelId 로 수렴 |
| 2 | X fetcher 실호출 확인 | 완료 | quote 트윗 1콜로 검증. 중첩 인용의 필드 누락을 발견해 수정(아래 §완료 상세) |
| 3 | reddit Apify actor 검증 | 완료 | body·crawledAt 실재 확인 → Apify 전환 완료. 실캡처 0 상태 해소 |
| 4 | instagram prodiger drop-in 검증 | 남음 | actor 교체 여부. 월 $3 차이라 급하지 않음 — 매핑 확장은 현행 actor 로 이미 완료 |
| 5 | 라우터 회귀 확인 | 완료 | router-cases 82건 · 전체 164/164 |
| 소셜 | 확인된 것 |
|---|---|
| youtube | oEmbed 는 channelId 를 안 줘 조인이 성립하지 않았다 → Data API v3 전환.
실측: watch?v=dQw4w9WgXcQ → UCuAXFkgsw1L7xaCfnd5JJOw ·
@nateherk → UC2ojq-nuP8ceeHqiroeKhBA ·
/user/PewDiePie → UC-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"] |
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 |
현재 youtube.fetcher.ts 는 무료 oEmbed 만 씁니다. oEmbed 응답에는
title·author_name·author_url·thumbnail_url 넷뿐이고
channelId 가 없습니다.
audit 이 확정한 YouTube 조인키가 channelId(UCxxx)인데, 지금 코드로는
author_url 의 @handle 밖에 못 얻습니다. handle 은 변경 가능하고 옛 핸들과 분기되므로
조인이 성립하지 않습니다. 게다가 구독자수·조회수·게시일도 전부 못 받습니다.
API 및 서비스 → 라이브러리 → "YouTube Data API v3" 검색 → 사용 클릭API 및 서비스 → 사용자 인증 정보 → + 사용자 인증 정보 만들기 → API 키API 제한사항 → "키 제한" 선택 → YouTube Data API v3만 체크.
애플리케이션 제한은 서버에서 쓰므로 "없음" 또는 IP 제한..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 를 정확히 구현할 수 있습니다.
응답에 개인정보는 없습니다(공개 채널·영상 메타데이터). 키 값만 가려서 주시면 됩니다.
디버깅 전용 엔드포인트가 이미 있습니다. 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 (필수) | — |
enablePaid | Apify 유료 게이트(tiktok·instagram). false 면 skipped_paid | false |
enableCommunity | X 커뮤니티 fetch (20 credits/호출, 느림) | false |
urlscanMode | search(가장 이른 기록) / scan(현재 렌더) | search |
{
"sourceType": "x_tweet", ← 분류가 맞나
"sourceKey": "2070...", ← 키가 맞나(리터럴 'playlist' 같은 게 아닌지)
"status": "ok",
"valueNature": { ... }, ← fixed/mutable 표기
"data": { ... } ← 실제 담긴 필드
}
fetch 없이 분류만 보고 싶을 때. 네트워크를 전혀 안 탑니다.
npx jest test/social-fetcher/router-cases.test.ts # 새 URL 을 케이스로 추가하려면 test/social-fetcher/router-cases.json 에 한 줄: # { "url": "...", "sourceType": "...", "sourceKey": "...", "group": "manual" }
# 인용(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 안전성을 잘못 보증 |
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" 로 뭉개졌습니다.
해당 계정이 복구됐을 수도 있으니 정지 계정이 아니면 그냥 정상 응답이 옵니다(그것도 정상).
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(=키조차 없음)보다 개선된 것입니다.
reddit.fetcher.ts 가 about.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 이 "문서에서 통째로 빠져 있었다"고 적발한 필드라 실재 여부 확인이 필요합니다.
actor 교체(현행 apify~instagram-scraper → prodiger/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. 다르면 현행 유지(비용차가 무의미).
# social-fetcher 전체 (139개) npx jest test/social-fetcher # 타입 체크 — social/twitter 관련만 필터(레포에 무관한 사전 에러가 다수 있음) npx tsc --noEmit -p tsconfig.json 2>&1 | grep -icE 'social|twitter' # 기대: 0
| 항목 | 내용 | 블록 |
|---|---|---|
| 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콜 |
.env.dev 에서 읽어오는 형태입니다.enablePaid=true 를 명시할 때만 발생합니다. 기본값은 false(=skipped_paid)입니다.enableCommunity=true 를 따로 켜야 합니다.