콘텐츠로 이동

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 오디오·악보·음성·음원 자산

categorytype이 위 표와 맞지 않으면 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

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를 본다.


게임 썸네일 자동 생성

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

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 | 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 초안 → 게시

  1. 업로드에서 HTML5 ZIP 또는 ZWF 패키지를 업로드합니다.
  2. POST /contentscategory: "jump", type: "game", jump.status: "draft"인 초안을 만듭니다.
  3. 소유자 인증으로 게임 실행을 확인한 뒤 아래 게시 요청을 보냅니다.

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.statuspublished가 됩니다. 패키지 파일과 실행 진입점을 다시 검사한 후 공개합니다. 여러 번 또는 동시에 요청해도 중복 Thread를 만들지 않으며 최초 공개 시각을 유지합니다. 일반 저장도 최초 공개 시각을 바꾸지 않습니다.

HTTP 상황
401 인증되지 않음
404 없는 콘텐츠, 다른 작성자의 콘텐츠 또는 아카이브
409 게시 가능한 초안이 아니거나 심사 대기·반려·수정 요청 상태
422 실행 패키지가 없거나 유효하지 않음
503 저장 실패. 초안/Thread 일부만 게시되지 않도록 원자적으로 처리

초안은 소유자 미리보기 외의 콘텐츠·게임 조회, 공개 피드·검색·다른 사용자 프로필·Thread에서 숨깁니다. 업로드 URL 자체는 공개 파일 주소이므로 비밀 파일 보관 용도로 사용하지 마세요.