페이지네이션과 cursor
서로 다른 두 가지 방식이 있습니다. 하려는 일에 맞게 고르세요.
| 하려는 일 | 사용 | 위치 표시 |
|---|---|---|
| 최신순으로 페이지를 넘기며 둘러보기 | /v1/structured, /v1/updates |
불투명한 cursor |
| 늦게 온 글이나 수정된 글까지 놓치지 않고 동기화 | /v1/feed |
숫자 after |
limit의 기본값은 20이며 모든 경로에서 1..100으로 제한됩니다.
cursor로 둘러보기
섹션 제목: “cursor로 둘러보기”/v1/structured와 /v1/updates는 최신순으로 반환합니다. 응답의 nextCursor를 cursor로 다시 보내면 다음(더 오래된) 페이지를 받습니다.
curl "https://api.chizumulu.net/v1/structured?limit=2"curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQwNDo0ODoxOC4wMDBafDI="구현 그대로의 동작입니다.
cursor는 불투명한 값입니다. 받은 값만 그대로 돌려보내세요. 해석할 수 없는 값은400cursor is invalid를 반환합니다.nextCursor는 페이지가 가득 찼을 때(항목이limit개) 들어 있습니다. 가득 찬 페이지는 더 있을 수도 있다는 뜻이며, 다음 페이지가 비어 있을 수도 있습니다. 항목이limit개보다 적으면nextCursor는null입니다.- 정렬은 메시지의 원본 타임스탬프와 API가 받은 시각 중 더 이른 쪽, 그다음
rawUpdateId순입니다. 그래서 미래로 적힌 원본 타임스탬프가 메시지를 맨 위에 고정시킬 수 없습니다.sourceTimestamp자체는 보낸 그대로 반환됩니다. /v1/structured필터(competition,teamId,teamAlias,team,postType)는 페이지를 자르기 전에 적용되므로, 필터를 건 페이지에는 조건에 맞는 항목이 최대limit개 담기고nextCursor규칙도 같습니다. 모든 페이지에 같은 필터를 보내세요.- cursor는 그 정렬에서의 위치를 가리킵니다. 이것은 뷰이지 동기화 수단이 아닙니다. 나중에 저장된 메시지가 더 새로운 위치를 가지면 cursor보다 앞쪽에 나타나며, cursor를 이어서 따라가도 반환되지 않습니다. 새 메시지를 보려면 첫 페이지를 다시 가져오세요.
/v1/updates는 대략적인 필터링을 위한 since(오프셋이 있는 타임스탬프)도 받습니다. since만으로는 안전한 이어가기를 표현할 수 없으므로 페이지를 넘길 때는 cursor를 쓰세요.
/v1/feed로 동기화하기
섹션 제목: “/v1/feed로 동기화하기”/v1/feed는 API가 저장한 순서대로 observation을 반환합니다(id는 계속 증가하며 오래된 것이 먼저). 원본 타임스탬프는 전혀 관여하지 않으므로, 늦게 도착한 글, 재전달된 글, 오래된 글의 수정본이 도착하는 대로 모두 반환됩니다.
after=0으로 시작합니다.- 받은
updates를 처리한 뒤nextAfter를 저장합니다. after=<nextAfter>로 다시 요청합니다.hasMore가false이면 일단 멈추고, 나중에 저장해 둔 같은 값에서 다시 폴링하세요.
nextAfter는 반환된 마지막 id(새 항목이 없으면 내가 보낸 after)이므로 언제든 저장해도 안전합니다. hasMore는 페이지가 가득 찼을 때(더 있을 수 있을 때) true입니다.
curl "https://api.chizumulu.net/v1/feed?after=3&limit=3"GET /v1/feed?after=3&limit=3
{ "updates": [ { "id": 4, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e04", "rawUpdateId": 4, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:20:54.451Z", "collectorObservedAt": "2026-09-19T15:20:54.140Z", "content": { "text": "88 GOAL! \n\nNorman Ndlovu completes his brace as Mafu Stars extend their lead! \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame" } }, { "id": 5, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e05", "rawUpdateId": 5, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70005", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:24:20.000Z", "receivedAt": "2026-09-19T15:24:22.805Z", "collectorObservedAt": "2026-09-19T15:24:22.493Z", "content": { "text": "Fulltime \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame" } }, { "id": 6, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e06", "rawUpdateId": 4, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:26:31.207Z", "collectorObservedAt": "2026-09-19T15:26:30.911Z", "content": { "text": "88' GOAL! \n\nNorman Ndlovu completes his brace as Mafu Stars extend their lead! \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame" } } ], "nextAfter": 6, "hasMore": true}최소한의 반복문 예시입니다.
let after = 0; // persist this between runsfor (;;) { const res = await fetch(`https://api.chizumulu.net/v1/feed?after=${after}&limit=100`); const page = await res.json(); for (const observation of page.updates) handle(observation); after = page.nextAfter; if (!page.hasMore) break;}수정은 새 observation이므로 소비자는 두 버전을 모두 받습니다. rawUpdateId로 같은 메시지의 observation을 묶고, id가 가장 큰 것이 최신입니다.
관심 있는 메시지의 구조화 결과는 /v1/structured나 history 경로로 가져오세요.