ZUKU API — 콘텐츠 (Contents)¶
브랜드: ZUKU API · 베이스 URL
/api/v1
구현 기준:backend/rs/src/router.rs,backend/rs/src/models.rs
문서에 명시된 경로와 필드만 사용하세요. 새 필드는 API 응답과 해당 기능 문서를 확인하세요.
미디어 카테고리(hype / swipe / jump / vive / vine)를 하나의 콘텐츠 계약으로 다룬다. 업로드 URL은 media.md에서 발급받은 뒤 본 문서의 thumbnail_url / media_url / 메타 패키지 필드에 참조한다.
카테고리와 type¶
category |
허용 type |
설명 |
|---|---|---|
hype |
interactive_longform · horizontal_media · photo_media |
인터랙티브 롱폼 · 가로형 미디어 · 사진 |
swipe |
vertical_video |
세로형 숏폼 영상 |
jump |
game |
HTML5 ZIP·ZWF 게임 패키지 |
vive |
literary_work · serial_chapter · short_essay |
글·연재·에세이 |
vine |
audio_track · score_attach · voice_clip · utau_asset · virtual_idol |
오디오·악보·음성·음원 자산 |
category와 type이 위 표와 맞지 않으면 422 + VALIDATION_ERROR (field: "type").
연령 등급 age_rating: all | 12 | 15 | 18 (생성 시 기본값 all).
GET /api/v1/contents/{id}¶
공개 콘텐츠 상세를 조회합니다. JUMP 초안은 작성자만 Bearer 인증으로 미리 볼 수 있으며, 다른 사용자와 게스트에게는 404입니다. 아카이브된 콘텐츠도 공개 조회·피드·검색·Thread에서 제외됩니다. 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¶
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를 본다.
게임 썸네일 자동 생성¶
JUMP 게임은 등록 후 실제 게임 화면으로 썸네일을 생성합니다. HTML5 ZIP·ZWF와 레거시 SWF를 지원하며, 생성은 게시 상태를 바꾸지 않습니다. 기존 게임은 순차 검사하고 정상 이미지를 유지합니다. 직접 업로드한 이미지는 명시적으로 교체를 요청한 경우에만 자동 생성 이미지로 바뀝니다.
GET /api/v1/contents/{id}/thumbnail-job¶
작성자 인증이 필요합니다. 성공 응답의 data.thumbnail_job에서 진행 상태를 조회합니다.
| 필드 | 값 또는 의미 |
|---|---|
status |
none, pending, running, ready, preserved, failed |
mode |
audit는 기존 이미지 검사, generate는 새 캡처, 작업이 없으면 null |
thumbnail_url |
현재 표시할 이미지 URL |
thumbnail_origin |
legacy, manual, automatic |
attempt_count |
현재 작업 시도 횟수, 최대 3회 |
error_code |
실패 코드 또는 null |
can_generate |
생성 서비스 사용 가능 여부 |
updated_at |
마지막 상태 변경 시각 |
thumbnail_revision |
이미지 변경 순서를 나타내는 문자열 |
pending 또는 running일 때 5초 간격으로 조회할 수 있습니다. preserved는 기존 이미지가 유지된 상태입니다. 원본이 없거나 게임을 실행할 수 없으면 failed가 되며, 실패 화면을 성공한 썸네일로 취급하지 않습니다.
POST /api/v1/contents/{id}/thumbnail-job¶
작성자가 생성 또는 재생성을 요청합니다. 본문 기본값은 {}이며, 직접 올린 이미지를 교체할 때는 { "replace_manual": true }를 보냅니다. 성공 시 202 Accepted와 같은 data.thumbnail_job 형식을 반환합니다. 이미 생성 대기·진행 중이면 중복 작업을 만들지 않습니다.
직접 올린 이미지에 교체 의사 없이 요청하면 409 · MANUAL_THUMBNAIL_PROTECTED, 생성 서비스가 준비되지 않았으면 503 · THUMBNAIL_WORKER_UNAVAILABLE입니다. 다른 작성자의 게임과 보관된 게임은 404입니다.
캡처 도중 원본 패키지나 수동 이미지가 변경되면 이전 작업 결과를 반영하지 않습니다. 게임 원본 교체 시 필요한 캡처를 다시 예약하며, 공개된 게임의 게시 시각이나 순위 기준을 변경하지 않습니다.
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¶
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 | vive | vine |
type |
✅ | JSON 키 이름 type (Rust 필드 content_type) |
title |
✅ | 1–100자 (공백만 불가) |
description |
기본 "", 최대 500자. VIVE 본문은 최대 200,000자 |
|
thumbnail_url |
기본 "" — 보통 /uploads/… |
|
media_url |
string \| null, 기본 null |
|
tags |
기본 [], 최대 10개 |
|
age_rating |
기본 "all" |
|
hype |
카테고리가 hype일 때 메타 객체 (선택) | |
swipe |
카테고리가 swipe일 때 메타 객체 (선택) | |
jump |
JUMP 필수 | status: "draft"로 생성. 공개는 별도 게시 API 사용 |
publish_to_thread |
Thread 동시 게시 선택, 기본 true. JUMP는 공개 전까지 Thread에 노출되지 않음 |
파일 바이트 자체는 받지 않는다. 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 | zwf | 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, url? } — format: zip 또는 zwf. url에 업로드 URL 참조 |
play_count / rating_avg / rating_count |
통계 시드 |
status |
생성 시 draft 필수. 게시 성공 후 published |
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자, VIVE는 ≤ 200,000자 |
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}¶
소프트 아카이브. Bearer만, 소유자만 가능합니다. 공개 피드·검색·추천과 연결된 Thread 게시물에서 숨겨집니다.
성공 → 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 — 버전 · 변경 이력
JUMP 초안 → 게시¶
- 업로드에서 HTML5 ZIP 또는 ZWF 패키지를 업로드합니다.
POST /contents로category: "jump",type: "game",jump.status: "draft"인 초안을 만듭니다.- 소유자 인증으로 게임 실행을 확인한 뒤 아래 게시 요청을 보냅니다.
POST /api/v1/contents/{id}/publish¶
작성자 Bearer 세션 필수. 요청 본문은 필요하지 않습니다.
curl -sS -X POST "https://zuzunza.com/api/v1/contents/$CONTENT_ID/publish" \
-H "Authorization: Bearer $ACCESS_TOKEN"
성공은 200 · data.content이며 jump.status가 published가 됩니다. 패키지 파일과 실행 진입점을 다시 검사한 후 공개합니다. 여러 번 또는 동시에 요청해도 중복 Thread를 만들지 않으며 최초 공개 시각을 유지합니다. 일반 저장도 최초 공개 시각을 바꾸지 않습니다.
| HTTP | 상황 |
|---|---|
401 |
인증되지 않음 |
404 |
없는 콘텐츠, 다른 작성자의 콘텐츠 또는 아카이브 |
409 |
게시 가능한 초안이 아니거나 심사 대기·반려·수정 요청 상태 |
422 |
실행 패키지가 없거나 유효하지 않음 |
503 |
저장 실패. 초안/Thread 일부만 게시되지 않도록 원자적으로 처리 |
초안은 소유자 미리보기 외의 콘텐츠·게임 조회, 공개 피드·검색·다른 사용자 프로필·Thread에서 숨깁니다. 업로드 URL 자체는 공개 파일 주소이므로 비밀 파일 보관 용도로 사용하지 마세요.