콘텐츠로 이동

ZUKU API — Feeds & Community Posts

미디어 피드(Hype / Swipe / Jump)와 커뮤니티 짧은 글(posts) API입니다. backend/rs/src/router.rs 실제 경로만 기술합니다.

Base URL

환경 Base URL
프로덕션 https://zuzunza.com/api/v1
로컬 http://localhost:3001/api/v1

목차

  1. 개요
  2. 페이지네이션
  3. 엔드포인트 표
  4. Media Feeds
  5. Community Feed
  6. Posts
  7. curl · JS/TS 예제

1. 개요

영역 경로 접두사 설명
미디어 피드 /feeds, /feeds/{category} 작품(콘텐츠) 목록. page / per_page
커뮤니티 타임라인 /feed 짧은 글 타임라인. 커서 before
커뮤니티 글 /posts… 단건·스레드·답글·좋아요·삭제

인증:

  • 읽기(feeds, feed, post 단건/스레드): 선택적 Bearer. 로그인 시 is_liked 등 뷰어 상태가 채워질 수 있음.
  • 쓰기(작성·답글·좋아요·삭제): Bearer 필수 (require_authenticated_user). X-API-Key는 이 경로들에 사용하지 않습니다.

응답은 공통 봉투 { success, data, meta } / { success, error, meta }를 따릅니다. 삭제는 성공 시 204 No Content.

참고: GET /api/v1/posts(목록) 경로는 없습니다. 목록은 GET /feed 또는 GET /users/me/posts를 사용합니다.


2. 페이지네이션

page / per_page (미디어 피드)

쿼리 기본 규칙
page 1 최소 1
per_page 20 최소 1

data.pagination 예:

{
  "page": 1,
  "per_page": 20,
  "total": 113405,
  "total_pages": 5671,
  "has_next": true,
  "has_prev": false,
  "next_cursor": null,
  "prev_cursor": null
}

미디어 /feeds 계열은 sort / order 쿼리를 사용하지 않습니다. 서버 고정 정렬입니다.

cursor (커뮤니티)

쿼리 기본 용도
limit 엔드포인트별 (아래) 페이지 크기 (> 0)
before 없음 타임라인·내 글: 이 id 이전(더 오래된) 쪽으로
after 없음 스레드: 이 id 이후(더 최신/이어지는) 쪽으로
엔드포인트 limit 기본 커서 필드
GET /feed 20 before → 응답 next_cursor
GET /posts/{id}/thread 50 after → 응답 next_cursor
POST 계열

next_cursor는 이번 페이지 마지막 항목 id입니다. 빈 페이지면 null → 클라이언트가 “더 불러오기”를 끄면 됩니다.


3. 엔드포인트 표

Method Path Auth 설명
GET /feeds 선택 전체/카테고리 미디어 피드
GET /feeds/hype 선택 Hype만
GET /feeds/swipe 선택 Swipe만
GET /feeds/jump 선택 Jump만
GET /feed 선택 커뮤니티 타임라인
POST /posts Bearer 루트 글 작성 → 201
GET /posts/{id} 선택 단건
PATCH /posts/{id} Bearer 내 글 본문·사진 부분 수정
GET /posts/hashtags 없음 최근 7일 인기 태그·접두사 검색
GET /posts/{id}/thread 선택 스레드(루트+답글)
POST /posts/{id}/replies Bearer 답글 → 201
POST /posts/{id}/like Bearer 좋아요 설정
DELETE /posts/{id}/like Bearer 좋아요 해제
DELETE /posts/{id} Bearer 소프트 삭제 → 204

4. Media Feeds

GET /feeds

쿼리:

파라미터 설명
category hype | swipe | jump (잘못된 값은 전체와 동일하게 처리될 수 있음 — Category::from_str 실패 시 전체)
page, per_page 위 표 참고

응답 data:

{
  "feeds": [ /* Content[] */ ],
  "pagination": { }
}

swipe는 전용 목록 함수(list_swipe_feed)를 탑니다. 그 외 카테고리/전체는 list_contents입니다. 변환(conversion) 메타는 서버가 첨부합니다.

단축 경로

Path 동작
GET /feeds/hype category=hype와 동일
GET /feeds/swipe swipe 전용 피드
GET /feeds/jump category=jump와 동일

단축 경로도 page / per_page를 동일하게 받습니다.


5. Community Feed (GET /feed)

커뮤니티 짧은 글 타임라인입니다.

GET /feed?limit=20&before=post_xxx

응답 data:

{
  "posts": [ /* Post[] */ ],
  "next_cursor": "post_last_id_or_null"
}

Post 필드 요약: id, author, body, image_urls, parent_id, root_id, like_count, reply_count, is_liked, is_deleted, created_at, updated_at

본문 최대 길이: 280자 (POST_MAX_CHARS).


6. Posts

POST /posts

{ "body": "안녕하세요 #ZUKU", "image_urls": ["/uploads/2026-09/이미지-id.jpg"] }
  • 201: data.post
  • 본문 최대 280자, 사진 최대 4장. 사진만 있는 글도 가능합니다.
  • 사진은 본인이 업로드한 10MB 이하 JPEG·PNG·WEBP·GIF·AVIF 파일이어야 합니다.
  • 검증 실패: 422 (본문 길이·사진 수·소유권·크기 등)
  • 미인증: 401

GET /posts/{id}

  • 성공: data.post
  • 없음: 404 POST_NOT_FOUND

GET /posts/{id}/thread

답글 id로 호출해도 그 글이 속한 스레드(root_id)를 반환합니다. 화면은 parent_id로 트리를 구성합니다.

쿼리: limit(기본 50), after

응답:

{
  "root_id": "…",
  "posts": [ ],
  "next_cursor": "…"
}

POST /posts/{id}/replies

부모(또는 스레드 내 글) id에 답글. { "body": "…", "image_urls": [] } 형식이며 루트 글과 같은 사진 규칙을 사용합니다.
부모 없음 → 404 POST_NOT_FOUND.

Like

Method 의미
POST /posts/{id}/like 좋아요 ON
DELETE /posts/{id}/like 좋아요 OFF

응답:

{ "is_liked": true, "like_count": 12 }

DELETE /posts/{id}

소프트 삭제(답글 자리 보존). 소유자만 성공.
남의 글·없는 글 모두 404 POST_NOT_FOUND(존재 여부 누출 방지).
성공: 204.


7. curl · JS/TS 예제

미디어 피드

curl -sS "http://localhost:3001/api/v1/feeds?category=hype&page=1&per_page=20"
curl -sS "http://localhost:3001/api/v1/feeds/swipe?page=1&per_page=10"
const base = "http://localhost:3001/api/v1";

const feedsRes = await fetch(
  `${base}/feeds?category=hype&page=1&per_page=20`,
);
const feedsJson = await feedsRes.json();
console.log(feedsJson.data.feeds.length, feedsJson.data.pagination);

커뮤니티 타임라인 (커서)

curl -sS "http://localhost:3001/api/v1/feed?limit=20"
curl -sS "http://localhost:3001/api/v1/feed?limit=20&before=POST_ID"
async function loadTimeline(before?: string) {
  const q = new URLSearchParams({ limit: "20" });
  if (before) q.set("before", before);
  const res = await fetch(`${base}/feed?${q}`);
  const json = await res.json();
  return json.data as { posts: unknown[]; next_cursor: string | null };
}

let cursor: string | null | undefined;
const page1 = await loadTimeline();
cursor = page1.next_cursor;
if (cursor) await loadTimeline(cursor);

글 작성 · 스레드 · 좋아요

curl -sS -X POST "http://localhost:3001/api/v1/posts" \
  -H "Authorization: Bearer $ACCESS" \
  -H "Content-Type: application/json" \
  -d '{"body":"첫 글입니다"}'

curl -sS "http://localhost:3001/api/v1/posts/$POST_ID/thread?limit=50"

curl -sS -X POST "http://localhost:3001/api/v1/posts/$POST_ID/like" \
  -H "Authorization: Bearer $ACCESS"
const accessToken = "…";

const createRes = await fetch(`${base}/posts`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ body: "첫 글입니다" }),
});
const { post } = (await createRes.json()).data;

const threadRes = await fetch(
  `${base}/posts/${post.id}/thread?limit=50`,
);
const thread = await threadRes.json();

await fetch(`${base}/posts/${post.id}/like`, {
  method: "POST",
  headers: { Authorization: `Bearer ${accessToken}` },
});

await fetch(`${base}/posts/${post.id}/replies`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ body: "답글입니다" }),
});

오류 코드 (이 문서 범위)

code HTTP 상황
UNAUTHORIZED 401 Bearer 필요 경로
POST_NOT_FOUND 404 글 없음/권한 없음(삭제 등)
VALIDATION_ERROR / 본문 검증 422 본문 길이 등
ROUTE_NOT_FOUND 404 미존재 경로

ZUKU API · Feeds & Posts · router.rs 기준

사진 수정과 해시태그

PATCH /posts/{id}는 소유자만 호출할 수 있으며 bodyimage_urls 중 전달한 필드만 바꿉니다. image_urls: []는 사진 전체 삭제입니다. 수정 결과에 본문과 사진이 모두 없으면 422이며 기존 글은 유지됩니다.

GET /feed?tag=게임은 본문의 #게임 태그를 정확히 검색합니다. q는 일반 본문 검색, tag가 함께 있으면 태그가 우선합니다. 한글·Unicode 태그를 지원하고 호환 문자와 영문 대소문자를 정규화합니다.

GET /posts/hashtags?q=게는 접두사에 맞는 최근 7일 태그를 최대 8개 반환합니다. data.tags 항목은 { tag, count, today_count, source: "community" }, data.window_days7입니다. 최근 하루 글 수, 7일 글 수 순이며 삭제된 글은 제외합니다.

JUMP 게임 목록 정렬

GET /api/v1/jump/gamessort, page, per_page, source, exclude_ids 등의 목록 쿼리를 사용합니다.

sort 동작
생략 · hot · recommended 신규 공개 게임을 먼저 표시한 뒤 기존 인기 추천
new · latest · recent 최초 공개 시각 내림차순
top 기존 인기순

신규 우선 기간은 최근 7일 공개 게임 수 N에 따라 min(7일, 7일 × 6 / max(N, 1))입니다. 기간 안의 게임은 최초 공개 시각 내림차순으로 먼저 나옵니다. 반응 수·썸네일 유무는 이 우선 순서를 뒤집지 않습니다. 기간이 정확히 만료되면 일반 추천 순서로 돌아갑니다.

빈도 N은 전체 공개 JUMP 게임을 기준으로 하므로 sourceexclude_ids 필터로 달라지지 않습니다. 초안·아카이브와 미래 공개 시각은 제외합니다. 오래 저장해 둔 초안을 오늘 처음 게시하면 새 게임으로 취급하며 재요청·저장으로 공개 시각을 갱신하지 않습니다.

페이지 순서는 요청 당시의 공개 상태·반응·시간을 반영합니다. pagination.has_nextnext_cursor를 사용하고, 서로 다른 필터의 커서를 재사용하지 마세요.