컨텐츠
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.
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.paginationPOST /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 — 버전 · 변경 이력