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} 선택 단건
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, 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" }
  • 201: data.post
  • 검증 실패: 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": "…" }.

부모 없음 → 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 기준*