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. 개요
| 영역 | 경로 접두사 | 설명 |
|---|---|---|
| 미디어 피드 | /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 기준*