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 등 게임 패키지

categorytype이 위 표와 맞지 않으면 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 — 버전 · 변경 이력