Zuku

피드

Feeds & Community

미디어 피드(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 (미디어 피드)

쿼리기본규칙
page1최소 1
per_page20최소 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 /feed20before → 응답 next_cursor
GET /posts/{id}/thread50after → 응답 next_cursor
POST 계열

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


3. 엔드포인트 표

MethodPathAuth설명
GET/feeds선택전체/카테고리 미디어 피드
GET/feeds/hype선택Hype만
GET/feeds/swipe선택Swipe만
GET/feeds/jump선택Jump만
GET/feed선택커뮤니티 타임라인
POST/postsBearer루트 글 작성 → 201
GET/posts/{id}선택단건
GET/posts/{id}/thread선택스레드(루트+답글)
POST/posts/{id}/repliesBearer답글 → 201
POST/posts/{id}/likeBearer좋아요 설정
DELETE/posts/{id}/likeBearer좋아요 해제
DELETE/posts/{id}Bearer소프트 삭제 → 204

4. Media Feeds

GET /feeds

쿼리:

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

응답 data:

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

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

단축 경로

Path동작
GET /feeds/hypecategory=hype와 동일
GET /feeds/swipeswipe 전용 피드
GET /feeds/jumpcategory=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: "답글입니다" }),
});

오류 코드 (이 문서 범위)

codeHTTP상황
UNAUTHORIZED401Bearer 필요 경로
POST_NOT_FOUND404글 없음/권한 없음(삭제 등)
VALIDATION_ERROR / 본문 검증422본문 길이 등
ROUTE_NOT_FOUND404미존재 경로

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

On this page