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} |
선택 | 단건 |
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:
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)¶
커뮤니티 짧은 글 타임라인입니다.
응답 data:
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¶
- 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
응답:
POST /posts/{id}/replies¶
부모(또는 스레드 내 글) id에 답글. { "body": "…", "image_urls": [] } 형식이며 루트 글과 같은 사진 규칙을 사용합니다.
부모 없음 → 404 POST_NOT_FOUND.
Like¶
| Method | 의미 |
|---|---|
POST /posts/{id}/like |
좋아요 ON |
DELETE /posts/{id}/like |
좋아요 OFF |
응답:
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}는 소유자만 호출할 수 있으며 body와 image_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_days는 7입니다. 최근 하루 글 수, 7일 글 수 순이며 삭제된 글은 제외합니다.
JUMP 게임 목록 정렬¶
GET /api/v1/jump/games는 sort, page, per_page, source, exclude_ids 등의 목록 쿼리를 사용합니다.
sort |
동작 |
|---|---|
생략 · hot · recommended |
신규 공개 게임을 먼저 표시한 뒤 기존 인기 추천 |
new · latest · recent |
최초 공개 시각 내림차순 |
top |
기존 인기순 |
신규 우선 기간은 최근 7일 공개 게임 수 N에 따라 min(7일, 7일 × 6 / max(N, 1))입니다. 기간 안의 게임은 최초 공개 시각 내림차순으로 먼저 나옵니다. 반응 수·썸네일 유무는 이 우선 순서를 뒤집지 않습니다. 기간이 정확히 만료되면 일반 추천 순서로 돌아갑니다.
빈도 N은 전체 공개 JUMP 게임을 기준으로 하므로 source나 exclude_ids 필터로 달라지지 않습니다. 초안·아카이브와 미래 공개 시각은 제외합니다. 오래 저장해 둔 초안을 오늘 처음 게시하면 새 게임으로 취급하며 재요청·저장으로 공개 시각을 갱신하지 않습니다.
페이지 순서는 요청 당시의 공개 상태·반응·시간을 반영합니다. pagination.has_next와 next_cursor를 사용하고, 서로 다른 필터의 커서를 재사용하지 마세요.