ZUKU API — Examples

인증·캡차·피드·콘텐츠·소셜을 잇는 실전 시나리오 모음입니다.

각 예제는 backend/rs/src/router.rs 경로를 기준으로 하며, Base는 프로덕션 https://zuzunza.com/api/v1입니다.

목차

  1. 공통 헬퍼
  2. 가입 → 로그인 → me
  3. 캡차 포함 가입
  4. 공개 피드 조회
  5. 콘텐츠 생성 · 업로드
  6. 좋아요 · 북마크 · 팔로우
  7. 댓글 · 알림 · DM
  8. Game Cloud (온라인 게임)
  9. 에러 처리 패턴

1. 공통 헬퍼

const API = "https://zuzunza.com/api/v1";

type Envelope<T> =
  | { success: true; data: T; meta: { request_id: string; timestamp: string; version: string } }
  | {
      success: false;
      error: { code: string; message: string; details?: { field: string; message: string }[] };
      meta: { request_id: string; timestamp: string; version: string };
    };

async function api<T>(
  path: string,
  init: RequestInit & { token?: string; apiKey?: string } = {},
): Promise<T> {
  const headers = new Headers(init.headers);
  headers.set("Accept-Language", "ko-KR");
  headers.set("X-Request-Id", crypto.randomUUID());
  if (init.body && !headers.has("Content-Type") && !(init.body instanceof FormData)) {
    headers.set("Content-Type", "application/json");
  }
  if (init.token) headers.set("Authorization", `Bearer ${init.token}`);
  if (init.apiKey) headers.set("X-API-Key", init.apiKey);

  const res = await fetch(`${API}${path}`, { ...init, headers });
  if (res.status === 204) return undefined as T;
  const env = (await res.json()) as Envelope<T>;
  if (!env.success) {
    const err = new Error(`${env.error.code}: ${env.error.message}`);
    (err as Error & { code?: string }).code = env.error.code;
    throw err;
  }
  return env.data;
}

2. 가입 → 로그인 → me

curl

# 로그인 (캡차 게이트 비활성 환경)
curl -sS -X POST "$API/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"identifier":"demo_user","password":"SecureP@ss123"}'

# me
curl -sS "$API/auth/me" -H "Authorization: Bearer $ACCESS"

JS/TS

const tokens = await api<{
  tokens: { access_token: string; refresh_token: string; expires_in: number };
}>("/auth/login", {
  method: "POST",
  body: JSON.stringify({ identifier: "demo_user", password: "SecureP@ss123" }),
});

const me = await api<{ user: { id: string; handle: string } }>("/auth/me", {
  token: tokens.tokens.access_token,
});

상세 필드: Authentication


3. 캡차 포함 가입

시크릿이 켜진 환경에서는 zcaptcha_token이 필요합니다.

const challenge = await api<{
  algorithm: string;
  challenge: string;
  salt: string;
  signature: string;
  maxnumber: number;
}>("/captcha/challenge", { method: "POST" });

// PoW 풀이 → base64 JSON 토큰 (프로토콜은 Captcha 문서 참고)
const zcaptcha_token = await solvePowInWorker(challenge);

await api("/auth/register", {
  method: "POST",
  body: JSON.stringify({
    email: "[email protected]",
    password: "SecureP@ss123",
    password_confirm: "SecureP@ss123",
    handle: "newcreator",
    zcaptcha_token,
  }),
});

전체 계약: Captcha


4. 공개 피드 조회

curl -sS "$API/feeds/hype?page=1&per_page=20"
curl -sS "$API/feeds?category=swipe&page=1&per_page=12&sort=hot"
curl -sS "$API/feed?limit=20"   # 커뮤니티 타임라인 (cursor: before)
const feed = await api<{ items: unknown[]; pagination: unknown }>(
  "/feeds/hype?page=1&per_page=8",
);

상세: Feeds


5. 콘텐츠 생성 · 업로드

업로드 (multipart)

curl -sS -X POST "$API/uploads" \
  -H "Authorization: Bearer $ACCESS" \
  -F "file=@./clip.mp4"
const fd = new FormData();
fd.append("file", fileBlob, "clip.mp4");
const uploaded = await api<{ url: string; mime: string; size: number }>(
  "/uploads",
  { method: "POST", body: fd, token: access },
);

콘텐츠 생성 (Bearer 또는 API 키)

curl -sS -X POST "$API/contents" \
  -H "Authorization: Bearer $ACCESS" \
  -H "Content-Type: application/json" \
  -d '{"category":"hype","type":"video","title":"첫 작품","media_url":"'"$URL"'"}'

상세: Contents · Media


6. 좋아요 · 북마크 · 팔로우

await api(`/contents/${id}/like`, { method: "POST", token: access });
await api(`/contents/${id}/bookmark`, { method: "POST", token: access });

// 크리에이터 팔로우 토글 (handle)
const follow = await api<{ is_following: boolean; follower_count: number }>(
  `/creators/${handle}/follow`,
  { method: "POST", token: access },
);

소셜 전반: Social


7. 댓글 · 알림 · DM

await api(`/contents/${id}/comments`, {
  method: "POST",
  token: access,
  body: JSON.stringify({ body: "좋은 작품이에요!" }),
});

const notifs = await api<{ notifications: unknown[] }>("/notifications", {
  token: access,
});

const dm = await api<{ conversation: { id: string } }>("/dm/conversations", {
  method: "POST",
  token: access,
  body: JSON.stringify({ handle: "friend_handle" }),
});

8. Game Cloud (온라인 게임)

Studio → Game Cloud에서 projectId를 만든 뒤:

const PROJECT = "YOUR_PROJECT_UUID";

// 지갑
const wallet = await api<{ POINT: number; CASH_KRW: number }>(
  `/cloud/economy/balance?projectId=${PROJECT}`,
  { token: access },
);

// 변수 (최고 점수)
await api("/cloud/vars/mutate", {
  method: "POST",
  token: access,
  body: JSON.stringify({
    projectId: PROJECT,
    scope: "user",
    key: "high_score",
    op: "max",
    num: 9999,
  }),
});

// 세이브
await api("/cloud/saves/save", {
  method: "POST",
  token: access,
  body: JSON.stringify({
    projectId: PROJECT,
    slot: "default",
    data: { level: 3, inventory: ["sword"] },
  }),
});

샘플: samples/cloud-rpg/ · 상세: Game Cloud


9. 에러 처리 패턴

try {
  await api("/auth/login", {
    method: "POST",
    body: JSON.stringify({ identifier, password }),
  });
} catch (e) {
  const code = (e as Error & { code?: string }).code;
  if (code === "CAPTCHA_FAILED") {
    // 새 challenge 후 재시도
  } else if (code === "UNAUTHORIZED") {
    // refresh 또는 재로그인
  } else if (code === "RATE_LIMITED") {
    // X-RateLimit-Reset 대기
  }
  throw e;
}

코드 표: Errors


다음 단계

문서 용도
Getting Started 봉투·Rate Limit·Versioning
Captcha PoW 세부
Authentication 세션·API 키
사이트 /docs/api/examples