콘텐츠로 이동

Raw와 Structured

API는 본 것거기서 파생한 것을 분리합니다.

  • 메시지(message) 는 원본의 논리적 게시물 하나입니다. rawUpdateId(숫자)와 sourceMessageId(원본 자체의 키)를 가집니다.
  • observation 은 수집기가 그 게시물을 본 한 번의 기록, 즉 그 시점의 텍스트(또는 캡션)입니다. 모든 observation에는 id(수집 순번, /v1/feed의 키)와 observationId(안정적인 문자열)가 있습니다.
  • observation은 받은 그대로 저장되며 수정하거나 삭제하지 않습니다.
  • 구조화 데이터는 observation 하나의 텍스트에서 파생됩니다. 별도의 층이며, 원문은 항상 그 옆에 함께 반환되고 대체되지 않습니다.

원본에서 게시물이 수정되면 수집기가 다시 보게 됩니다. API는 이를 같은 메시지의 또 하나의 observation으로 저장합니다(같은 rawUpdateId, 새로운 idobservationId, 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를 확인해 보세요.

Terminal window
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과 다릅니다.
  • 텍스트가 없는 done observation(예: 이미지만 있는 게시물)은 method: "none", structured: null입니다. 추출할 것이 없었기 때문입니다.

stateunregistered가 아니면 processing에는 다음도 들어 있습니다.

필드 의미
method 결과가 만들어진 방식: deterministic(규칙 기반 파서), ai(언어 모델. 파서가 읽을 수 있었다면 파서 결과의 제약을 받음), reused(텍스트가 같은 이전 observation에서 복사), none(처리할 텍스트 없음), done 전에는 null.
model ai 결과를 만든 모델, 아니면 null.
postType 결과와 함께 done이면 게시물 유형, 아니면 null.
translationState none, done, failed.
reusedFromObservationId methodreused일 때 설정됩니다.
error failed일 때의 짧은 진단 문구. 문구 자체에 의존하지 마세요.

처리 대기 중인 항목은 두 번째 페이지 예제에서 볼 수 있습니다.

korean은 게시물 전체 텍스트를 기계 번역한 것으로, 구조화 데이터와 함께 만들어집니다. 파생 데이터이므로 오류가 있을 수 있고, latestObservation.text를 대체하지 않습니다. observation이 done이고 번역이 있을 때만 값이 있으며, 그렇지 않으면 null입니다.