ZUKU API — 콘텐츠 (Contents)
> 브랜드: ZUKU API · 베이스 URL /api/v1
> 구현 기준: backend/rs/src/router.rs, backend/rs/src/models.rs
> 이 문서에 없는 경로는 구현되지 않은 것으로 취급한다. 임의로 엔드포인트를 가정하지 말 것.
미디어 3종 카테고리(hype / swipe / jump)를 하나의 콘텐츠 계약으로 다룬다. 업로드 URL은 media.md에서 발급받은 뒤 본 문서의 thumbnail_url / media_url / 메타 패키지 필드에 참조한다.
카테고리와 type
category |
허용 type |
설명 |
|---|---|---|
hype |
interactive_longform · horizontal_media · photo_media |
인터랙티브 롱폼 · 가로형 미디어 · 사진 |
swipe |
vertical_video |
세로형 숏폼 영상 |
jump |
game |
WASM/HTML5 등 게임 패키지 |
category와 type이 위 표와 맞지 않으면 422 + VALIDATION_ERROR (field: "type").
연령 등급 age_rating: all | 12 | 15 | 18 (생성 시 기본값 all).
GET /api/v1/contents/{id}
콘텐츠 상세. 공개 조회 가능. Authorization: Bearer …가 있으면 is_liked / is_bookmarked / is_following_creator 등 뷰어 스코프 필드가 채워진다. 변환 정보가 있으면 content.conversion에 첨부된다.
응답 data
{
"content": {
"id": "…",
"category": "hype",
"type": "horizontal_media",
"title": "…",
"description": "…",
"thumbnail_url": "/uploads/…",
"media_url": "/uploads/…",
"creator": { "id": "…", "display_name": "…", "handle": "…", "avatar_url": "…", "is_verified": false },
"stats": { "like_count": 0, "comment_count": 0, "view_count": 0, "share_count": 0, "bookmark_count": 0 },
"tags": [],
"age_rating": "all",
"is_liked": false,
"is_bookmarked": false,
"is_following_creator": false,
"created_at": "…",
"updated_at": "…",
"hype": null,
"swipe": null,
"jump": null,
"conversion": null
}
}
없는 ID → 404 · CONTENT_NOT_FOUND.
curl
curl -sS "https://zuzunza.com/api/v1/contents/{id}" \
-H "Accept: application/json"
JS/TS (fetch)
const res = await fetch(`/api/v1/contents/${id}`, {
headers: { Accept: 'application/json' },
});
const envelope = await res.json();
if (!envelope.success) throw new Error(envelope.error.code);
const { content } = envelope.data;
GET /api/v1/contents/{id}/conversion
레거시 SWF/FLV 등에서 파생된 변환 상태만 조회한다. media_url 원본과 별개 계약이다.
응답 data: { "conversion": ConversionInfo }
| 필드 | 의미 | |
|---|---|---|
status |
변환 파이프라인 상태 문자열 | |
converter_version |
변환기 버전 | |
playback_url |
재생 URL (없으면 null/생략) |
|
preview |
미리보기 여부 | |
poster_url / thumbnail_url |
포스터·썸네일 | |
duration_sec |
길이(초) | |
source_kind |
animation \ |
game |
error_code / message |
실패 시 |
콘텐츠에 변환 레코드가 없으면 404 · CONTENT_NOT_FOUND.
자세한 Jump 재생·IR 스트림은 media.md를 본다.
GET /api/v1/contents/{id}/recommendations
관련 추천 목록.
| 쿼리 | 기본 | 범위 | 설명 |
|---|---|---|---|
limit |
8 |
1–50 (클램프) | 한 번에 가져올 개수 |
offset |
0 |
≥ 0 | 건너뛸 개수 |
응답 data
{
"recommendations": [ /* Content[] */ ],
"pagination": {
"total": 0,
"limit": 8,
"offset": 0,
"has_more": false
}
}
기준 콘텐츠가 없으면 404 · CONTENT_NOT_FOUND.
has_more 는 LIMIT+1 로 계산한다. total 은 더 있을 때 하한(offset+limit+1)이다.
점프 상세는 GET /jump/games/{id}?include=related,popular 로 같은 선반을 한 번에 받을 수 있다.
curl
curl -sS "https://zuzunza.com/api/v1/contents/{id}/recommendations?limit=8&offset=0"
JS/TS
const q = new URLSearchParams({ limit: '8', offset: '0' });
const res = await fetch(`/api/v1/contents/${id}/recommendations?${q}`);
const { data } = await res.json();
// data.recommendations, data.pagination
POST /api/v1/contents
콘텐츠 생성. 인증 필수: Authorization: Bearer <access_token> 또는 X-API-Key: <developer_key>.
성공 → 201 Created · data.content.
CreateContentRequest 필드 (models.rs)
| 필드 | 필수 | 비고 | ||
|---|---|---|---|---|
category |
✅ | hype \ |
swipe \ |
jump |
type |
✅ | JSON 키 이름 type (Rust 필드 content_type) |
||
title |
✅ | 1–100자 (공백만 불가) | ||
description |
기본 "", 최대 500자 |
|||
thumbnail_url |
기본 "" — 보통 /uploads/… |
|||
media_url |
`string \ | null, 기본 null` |
||
tags |
기본 [], 최대 10개 |
|||
age_rating |
기본 "all" |
|||
hype |
카테고리가 hype일 때 메타 객체 (선택) | |||
swipe |
카테고리가 swipe일 때 메타 객체 (선택) | |||
jump |
카테고리가 jump일 때 메타 객체 (선택) |
파일 바이트 자체는 받지 않는다. media.md에서 URL을 받은 뒤 참조한다.
hype (HypeMeta)
| 필드 | 설명 | ||||
|---|---|---|---|---|---|
hype_type |
quiz \ |
poll \ |
story \ |
challenge \ |
interaction |
wasm? |
{ package_url, memory_cap_mb, entry_point } |
||||
video? |
{ duration_sec, aspect_ratio, quality } — quality: S\ |
A\ |
B\ |
C |
|
gallery? |
{ image_urls: string[] } |
swipe (SwipeMeta)
| 필드 | 설명 | ||
|---|---|---|---|
duration_sec |
길이(초) | ||
resolution |
해상도 문자열 | ||
orientation |
portrait \ |
landscape \ |
square |
aspect_ratio |
비율 문자열 | ||
quality |
품질 등급 | ||
auto_play |
기본 true |
||
loop |
기본 true (JSON 키 loop) |
||
audio_credit? |
음원 크레딧 |
jump (JumpMeta)
| 필드 | 설명 | ||||
|---|---|---|---|---|---|
game_id |
게임 식별자 | ||||
game_type |
html5 \ |
wasm \ |
unity \ |
godot \ |
other |
genre |
장르 | ||||
distribution_mode |
online \ |
offline \ |
both |
||
platform |
{ pc, mobile, tablet } |
||||
mobile_optimized |
{ certified, level? } |
||||
package |
{ format, entry_point, size_bytes, hash, version } — format 예: zip |
||||
play_count / rating_avg / rating_count |
통계 시드 | ||||
status |
게시 상태 머신 문자열 |
curl
curl -sS -X POST "https://zuzunza.com/api/v1/contents" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"category": "swipe",
"type": "vertical_video",
"title": "첫 숏폼",
"description": "설명",
"thumbnail_url": "/uploads/2026-08/….jpg",
"media_url": "/uploads/2026-08/….mp4",
"tags": ["demo"],
"age_rating": "all",
"swipe": {
"duration_sec": 15,
"resolution": "1080x1920",
"orientation": "portrait",
"aspect_ratio": "9:16",
"quality": "A",
"auto_play": true,
"loop": true
}
}'
개발자 키 예시:
curl -sS -X POST "https://zuzunza.com/api/v1/contents" \
-H "X-API-Key: $ZUKU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ … }'
JS/TS
async function createContent(token: string, body: unknown) {
const res = await fetch('/api/v1/contents', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const envelope = await res.json();
if (!envelope.success) {
throw Object.assign(new Error(envelope.error.message), {
code: envelope.error.code,
details: envelope.error.details,
status: res.status,
});
}
return envelope.data.content;
}
PATCH /api/v1/contents/{id}
부분 수정. Bearer만 허용 (X-API-Key 불가). 작성자 본인 콘텐츠만 갱신.
UpdateContentRequest — 전달한 필드만 갱신:
| 필드 | 검증 | |||
|---|---|---|---|---|
title? |
1–100자 | |||
description? |
≤ 500자 | |||
tags? |
≤ 10개 | |||
age_rating? |
all\ |
12\ |
15\ |
18 |
thumbnail_url? |
문자열 |
성공 → 200 · data.content. 없거나 권한 없음 → 404 · CONTENT_NOT_FOUND (존재 여부 구분 없음).
curl
curl -sS -X PATCH "https://zuzunza.com/api/v1/contents/{id}" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"제목 수정","tags":["a","b"]}'
JS/TS
const res = await fetch(`/api/v1/contents/${id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ title: '제목 수정' }),
});
const envelope = await res.json();
if (!envelope.success) throw new Error(envelope.error.code);
DELETE /api/v1/contents/{id}
소프트 아카이브 (archive_content). 하드 삭제가 아니다. Bearer만.
성공 → 200 · 아카이브된 data.content.
curl
curl -sS -X DELETE "https://zuzunza.com/api/v1/contents/{id}" \
-H "Authorization: Bearer $ACCESS_TOKEN"
좋아요 · 북마크 (요약)
소셜·상호작용의 상세 계약은 social.md를 본다. 여기서는 콘텐츠 경계만 요약한다.
| 메서드 | 경로 | 인증 | 동작 |
|---|---|---|---|
POST |
/api/v1/contents/{id}/like |
Bearer | 좋아요 토글 → { is_liked, like_count } |
POST |
/api/v1/contents/{id}/bookmark |
Bearer | 북마크 토글 → { is_bookmarked, bookmark_count } |
미인증 → 401 · UNAUTHORIZED. 없는 콘텐츠 → 404 · CONTENT_NOT_FOUND.
관련 페이지
- media.md — 업로드 · Jump play/stream/swf · conversion
- errors.md — 봉투 · 상태 코드 · 에러 코드
- changelog.md — 버전 · 변경 이력