통합 트렌드 콘텐츠 레코드
YouTube + TikTok + Instagram 을 목업 23필드 단일 스키마로 정규화해 한 번에 내려주는 통합 API.
통합 트렌드 콘텐츠 레코드
세 플랫폼(YouTube·TikTok·Instagram)의 트렌드 콘텐츠를 하나의 "콘텐츠 레코드"(23필드) 스키마로 정규화해 한 번에 반환합니다. 수집·적재는 파이썬(sns-crawler-python), 서빙은 백엔드가 PostgreSQL 원본 테이블을 직접 읽습니다.
- 플랫폼당 최근
perPlatform건(기본 100)씩 가져와 재현가능 셔플로 섞어 내려줍니다. total= 3소스 전체 레코드 수(현재 필터 기준).
HTTP 요청
GET /api/trend/records?region=KR&category=BEAUTY&perPlatform=100Query Parameters
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
region | String | 아니오 | 지역(YouTube region / TikTok country_code). Instagram(post)은 지역 컬럼이 없어 미적용 |
platform | String | 아니오 | YOUTUBE | TIKTOK | INSTAGRAM 단일 필터. 미지정=3개 전체 |
category | String | 아니오 | 분야 BEAUTY | FASHION | TRAVEL. 미지정=전체 |
perPlatform | Integer | 아니오 | 플랫폼당 최근 N건(1~500, 기본 100) |
category 는 3플랫폼에 각각 적용됩니다 — YouTube=keyword_slot, TikTok=tiktok_trend_hashtag.category 조인, Instagram=분야별 한글 키워드로 캡션(caption) ILIKE 검색(YT/TikTok처럼 미리 수집된 분야가 없어 라이브 검색).
응답 (200 OK)
표준 ApiResponse 래퍼. 실제 데이터는 data.
{
"status": 200, "code": null, "message": "조회 완료",
"data": {
"total": 13672,
"counts": { "youtube": 8200, "tiktok": 3100, "instagram": 2372 },
"returned": 300,
"records": [
{
"source": "tiktok_trend", "platform": "TIKTOK",
"record_id": "7412...", "account_pk": "beauty_kr", "account": "뷰티크리에이터",
"cluster_id": null, "taken_at": null, "lang": "ko", "region": "KR",
"media_type": "VIDEO", "duration_s": 23, "hashtag_n": 5,
"audio_id": "70123...", "like_count": null, "comment_count": null,
"share_count": null, "save_count": null, "play_count": null,
"eng_rate": null, "delta_6h": null, "snapshot_seq": null,
"captured_at": "2026-07-27T02:10:00Z", "status": "ACTIVE",
"title": "여름 데일리 메이크업 #메이크업 #grwm",
"url": "https://www.tiktok.com/@beauty_kr/video/7412...",
"thumbnail": "https://...", "audio_title": "Catch Catch", "audio_author": "YENA"
},
{
"source": "youtube_trend", "platform": "YOUTUBE",
"record_id": "5xQI8KouLPc", "account_pk": "UC7h...", "account": "스타템픽",
"taken_at": "2026-07-25T12:00:00Z", "lang": null, "region": "KR",
"media_type": "SHORTS", "duration_s": 14, "hashtag_n": 2,
"audio_id": "sjtyqmA-pH4", "like_count": 12030, "comment_count": 210,
"share_count": null, "save_count": null, "play_count": 450050,
"eng_rate": 0.027197, "delta_6h": 8400, "snapshot_seq": 3,
"captured_at": "2026-07-27", "status": "public",
"title": "여자들이 한순간에 반한 이유 #메이크업", "url": "https://www.youtube.com/watch?v=5xQI8KouLPc",
"thumbnail": "https://i.ytimg.com/vi/5xQI8KouLPc/hqdefault.jpg"
}
]
}
}콘텐츠 레코드 필드 (플랫폼별 채움)
✅=값 있음 · ≈=파생/계산 · ∅=해당 플랫폼 미제공(null)
| 필드 | YouTube | TikTok | 비고 | |
|---|---|---|---|---|
source / platform | ✅ | ✅ | ✅ | 원본 테이블 / 플랫폼 |
record_id | video_id | item_id | post_id | 콘텐츠 id |
account_pk / account | channel_id / title | author_unique_id / name | account_id / handle | |
cluster_id | ∅ | ∅ | ∅ | 분석 파생(수집 원천 아님) |
taken_at | published_at | ∅ | published_at | TikTok trend_video 엔 게시일 없음 |
lang | default_audio_language | textLanguage | ∅ | YT도 대부분 null |
region | ✅ | country_code | ∅ | TikTok=수집 조합 국가(v.region=locationCreated 와 다름). post 엔 지역 컬럼 없음 |
media_type | SHORTS/VIDEO | VIDEO | IMAGE/VIDEO | |
duration_s | ✅ | ✅ | ∅ | TikTok=영상 길이(music_duration=음원 길이라 별개) |
hashtag_n | ≈ | hashtag_n 컬럼 | ≈ | YT/IG=제목·캡션의 # 수(서버 계산), TikTok=컬럼 값(없으면 계산) |
audio_id | 쇼츠 사운드 | music_id ✅ | ∅ | TikTok=음원 강점, IG post=음원 없음 |
like_count / comment_count | ✅ | ∅ | post_metrics ✅ | |
share_count / save_count | ∅ | ∅ | post_metrics ✅ | YT 미제공 |
play_count | view_count | ∅ | views(post_metrics) | |
eng_rate | ≈ | ∅ | ≈ | (like+comment)/play 계산 |
delta_6h / snapshot_seq | view_delta / snapshot_count | ∅ | ∅ | YT만 롤링 델타 보유 |
captured_at | collected_date | collected_at | crawled_at | |
status | privacyStatus | ACTIVE/PRIVATE/REVIEWING/REMOVED | sponsored 여부 | |
title / url / thumbnail | ✅ | caption / post_link / cover | caption / post_link / ∅ | 렌더용 확장 필드 |
각 플랫폼 강점: YouTube=조회수·델타, TikTok=음원, Instagram=engagement(post_metrics).
null 이 될 수 있는 필드
플랫폼마다 원천이 제공하는 값이 달라 아래 필드는 null 로 내려갈 수 있습니다. FE는 모든 지표/부가 필드를 null 허용으로 처리하세요(빈칸·-·기본값).
모든 플랫폼 공통
cluster_id— 분석 파생값(클러스터링 결과)이라 수집 원천에 없음 → 현재 항상 null.
구조상 항상 null (해당 플랫폼이 원천에서 제공 안 함)
| 플랫폼 | 항상 null 필드 | 이유 |
|---|---|---|
| YouTube | share_count, save_count | YouTube API 미제공 |
| TikTok | taken_at, like_count, comment_count, share_count, save_count, play_count, eng_rate, delta_6h, snapshot_seq | tiktok_trend_video 엔 게시일·지표 컬럼이 없음(Creative Center 트렌드 스냅샷이라 조회수·좋아요는 item_id 로 나중에 보강 예정) |
region, duration_s, audio_id, delta_6h, snapshot_seq, lang, thumbnail | post 엔 지역·음원 컬럼 없음, 썸네일은 v1 미파싱 |
조건부 null (값이 있을 때만 채움)
lang(YouTube) —default_audio_language는 유튜브가 잘 안 채워 대부분 null.audio_id(YouTube) — 쇼츠가 라이브러리 음원을 쓴 경우만. 오리지널 사운드·롱폼은 null.lang·duration_s·status(TikTok) — 수집 시점에 원본이 비어있던 행은 null.status— YouTube는 원본privacyStatus없으면 null(대개public), TikTok은ACTIVE/PRIVATE/REVIEWING/REMOVED(서버는 걸러내지 않으니 FE에서 노출 정책 결정), Instagram은 스폰서 게시물일 때만"sponsored"아니면 null.like/comment/share/save/play_count(Instagram) —post_metrics조인 결과라 해당 게시물 metrics 가 아직 수집 안 됐으면 null(LEFT JOIN).eng_rate—play_count(재생수)가 없거나like·comment가 모두 없으면 null.- YouTube가 v3 쿼터 소진으로 YT-API fallback 수집된 행은
like_count·comment_count·lang·status등이 null 일 수 있음.
어떤 플랫폼 소스가 통째로 비어있으면(테이블에 데이터 없음/조회 실패) 그 플랫폼 레코드는 응답에서 아예 빠집니다(필드가 null이 아니라 레코드 자체가 없음). 플랫폼별 실제 건수는 counts 로 확인하세요.
동작 상세
- 재현가능 셔플: 반환 레코드는 현재 레코드 집합에서 유도한 시드로 셔플합니다. 데이터가 그대로면 매 요청 순서가 동일하고, 레코드가 추가/삭제/변경되면 순서가 바뀝니다(FE 캐싱·페이지 안정성 목적).
- 데이터소스: YouTube=
youtube_trend(agent PG), TikTok=tiktok_trend_video·Instagram=post(+post_metrics)(crawl PG). prod는 동일 물리 DB, test는 분리 DB(코드는 두 데이터소스로 정상 동작). - 부분 허용: 한 플랫폼 소스가 비어있거나 조회 실패해도 나머지는 정상 서빙(counts 에 반영).
- DDL 없음 — 세 테이블 모두 이미 수집·존재.
Instagram 캡션 검색(category 지정 시)은 post.caption 에 인덱스가 없어 데이터가 많으면 느릴 수 있습니다(전량이 아니라 최근 perPlatform 건만 반환하도록 스코프). 성능 이슈 시 pg_trgm GIN 인덱스가 후속 옵션입니다.