Raw와 Structured
메시지와 observation
섹션 제목: “메시지와 observation”API는 본 것과 거기서 파생한 것을 분리합니다.
- 메시지(message) 는 원본의 논리적 게시물 하나입니다.
rawUpdateId(숫자)와sourceMessageId(원본 자체의 키)를 가집니다. - observation 은 수집기가 그 게시물을 본 한 번의 기록, 즉 그 시점의 텍스트(또는 캡션)입니다. 모든 observation에는
id(수집 순번,/v1/feed의 키)와observationId(안정적인 문자열)가 있습니다. - observation은 받은 그대로 저장되며 수정하거나 삭제하지 않습니다.
- 구조화 데이터는 observation 하나의 텍스트에서 파생됩니다. 별도의 층이며, 원문은 항상 그 옆에 함께 반환되고 대체되지 않습니다.
원본에서 게시물이 수정되면 수집기가 다시 보게 됩니다. API는 이를 같은 메시지의 또 하나의 observation으로 저장합니다(같은 rawUpdateId, 새로운 id와 observationId, observationCount 증가). 이전 observation은 그대로 남습니다.
같은 텍스트를 원본이 다시 전달하는 등 다른 이유로도 같은 일이 생길 수 있습니다. API는 수집기가 관찰한 것을 기록할 뿐, 원본 자체의 수정 순서를 안다고 주장하지 않습니다.
경로별로 보이는 텍스트
섹션 제목: “경로별로 보이는 텍스트”| 경로 | 텍스트의 출처 |
|---|---|
/v1/updates, /v1/latest |
각 메시지의 첫 observation |
/v1/feed |
각 observation이 독립된 항목 |
/v1/structured |
각 메시지의 가장 최근 observation (“가장 최근” = 수집 id가 가장 큰 것이며 원본 타임스탬프가 아닙니다) |
/v1/structured/{rawUpdateId}/history |
모든 observation, 오래된 순, 최대 200개 |
아래에서 메시지 4는 수정되었습니다. 처음에는 88 GOAL!, 수정 후에는 88' GOAL!입니다. observationId, receivedAt, 그리고 각 observation의 구조화 결과에서 서로 다른 minuteText를 확인해 보세요.
curl "https://api.chizumulu.net/v1/structured/4/history"응답
{ "rawUpdateId": 4, "source": "whatsapp", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "observations": [ { "id": 4, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e04", "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:20:54.451Z", "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", "processing": { "state": "done", "method": "ai", "model": "deepseek/deepseek-v4-flash-0731", "postType": "live_update", "translationState": "done", "reusedFromObservationId": null, "error": null }, "structured": { "schemaVersion": 2, "postType": "live_update", "competition": { "code": "nrfa_league_one", "conflict": false, "evidence": [ { "code": "nrfa_league_one", "signal": "#CINRFALeagueOne", "kind": "hashtag" } ] }, "matches": [ { "index": 0, "homeTeam": "Mafu Stars FC", "awayTeam": "Luviri FC", "homeScore": 3, "awayScore": 1, "status": "live", "competition": null, "competitionCode": "nrfa_league_one", "competitionBasis": "post-signal", "venue": null, "dateText": null, "timeText": null, "roundText": null, "minuteText": null, "competitionGroupText": null, "evidence": { "text": true, "attachments": [] }, "events": [ { "index": 0, "type": "goal", "minuteText": null, "team": "Mafu Stars FC", "homeScore": null, "awayScore": null, "players": [ { "name": "Norman Ndlovu", "role": "scorer" } ] } ] } ], "facts": [] }, "korean": { "text": "88 골!\n\n노먼 은들로부가 멀티골을 완성하며 마푸 스타즈가 리드를 넓힙니다!\n\n마푸 스타즈 FC 3–1 루비리 FC\n\n#CINRFALeagueOne\n#NRFATransformingTheGame", "model": "deepseek/deepseek-v4-flash-0731" } }, { "id": 6, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e06", "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:26:31.207Z", "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", "processing": { "state": "done", "method": "ai", "model": "deepseek/deepseek-v4-flash-0731", "postType": "live_update", "translationState": "done", "reusedFromObservationId": null, "error": null }, "structured": { "schemaVersion": 2, "postType": "live_update", "competition": { "code": "nrfa_league_one", "conflict": false, "evidence": [ { "code": "nrfa_league_one", "signal": "#CINRFALeagueOne", "kind": "hashtag" } ] }, "matches": [ { "index": 0, "homeTeam": "Mafu Stars FC", "awayTeam": "Luviri FC", "homeScore": 3, "awayScore": 1, "status": "live", "competition": null, "competitionCode": "nrfa_league_one", "competitionBasis": "post-signal", "venue": null, "dateText": null, "timeText": null, "roundText": null, "minuteText": "88'", "competitionGroupText": null, "evidence": { "text": true, "attachments": [] }, "events": [ { "index": 0, "type": "goal", "minuteText": "88'", "team": "Mafu Stars FC", "homeScore": null, "awayScore": null, "players": [ { "name": "Norman Ndlovu", "role": "scorer" } ] } ] } ], "facts": [] }, "korean": { "text": "88' 골!\n\n노먼 은들로부가 멀티골을 완성하며 마푸 스타즈가 리드를 넓힙니다!\n\n마푸 스타즈 FC 3–1 루비리 FC\n\n#CINRFALeagueOne\n#NRFATransformingTheGame", "model": "deepseek/deepseek-v4-flash-0731" } } ]}같은 메시지를 /v1/updates(첫 observation의 텍스트)와 /v1/structured(가장 최근 observation의 텍스트)에서 비교해 보려면 엔드포인트 예제를 보세요.
처리 상태
섹션 제목: “처리 상태”observation은 각자 처리 과정을 거칩니다. 가장 최근 observation의 processing.state는 다음 중 하나입니다.
processing.state |
의미 | structured |
korean |
|---|---|---|---|
unregistered |
저장되었지만 아직 접수되지 않음. processing에는 state만 있습니다. |
null |
null |
pending |
접수되어 처리를 기다리거나 재시도 중. | null |
null |
awaiting_ai |
언어 모델 단계가 필요하지만 아직 실행되지 않음. | null |
null |
done |
처리 완료. | 결과가 있으면 그 결과 | 번역이 있으면 그 번역 |
failed |
처리를 포기함. processing.error에 짧은 진단 문구가 있습니다. 원문에는 영향이 없습니다. |
null |
null |
믿고 쓸 수 있는 규칙:
done이 아닌 observation에는structured가 절대 표시되지 않습니다. 특히 수정된 메시지에서 이전 observation의 팩트가 현재 상태로 보이는 일은 없습니다.done인 게시물도 경기와 팩트가 모두 0개일 수 있습니다(생일 축하글도 정상적인 결과입니다). 이것은structured: null과 다릅니다.- 텍스트가 없는
doneobservation(예: 이미지만 있는 게시물)은method: "none",structured: null입니다. 추출할 것이 없었기 때문입니다.
state가 unregistered가 아니면 processing에는 다음도 들어 있습니다.
| 필드 | 의미 |
|---|---|
method |
결과가 만들어진 방식: deterministic(규칙 기반 파서), ai(언어 모델. 파서가 읽을 수 있었다면 파서 결과의 제약을 받음), reused(텍스트가 같은 이전 observation에서 복사), none(처리할 텍스트 없음), done 전에는 null. |
model |
ai 결과를 만든 모델, 아니면 null. |
postType |
결과와 함께 done이면 게시물 유형, 아니면 null. |
translationState |
none, done, failed. |
reusedFromObservationId |
method가 reused일 때 설정됩니다. |
error |
failed일 때의 짧은 진단 문구. 문구 자체에 의존하지 마세요. |
처리 대기 중인 항목은 두 번째 페이지 예제에서 볼 수 있습니다.
한국어 번역
섹션 제목: “한국어 번역”korean은 게시물 전체 텍스트를 기계 번역한 것으로, 구조화 데이터와 함께 만들어집니다. 파생 데이터이므로 오류가 있을 수 있고, latestObservation.text를 대체하지 않습니다. observation이 done이고 번역이 있을 때만 값이 있으며, 그렇지 않으면 null입니다.