프론트엔드 변경 이력 — v3 → v4
프론트엔드 크루가 읽는 문서다. v3에서 v4로 넘어가며 화면과 코드가 실제로 바뀌는 것만 담았다. 왼쪽이 v3, 오른쪽이 v4다. 백엔드 쪽 변경은 BE 변경 이력에 따로 있다. 확정 계약은 아직 v3이고 v4는 검토 요청안이다. 근거가 되는 계약 본문은 v4-fe Client API에 있다.
먼저 알아야 할 두 가지
0고칠 곳은 /board 하나다 — 다만 파괴적 변경이다
GET /api/v1/routes
GET …/routes/{id}/board
GET …/routes/{id}/vehicles
// info.version: 1.0.0GET /api/v1/routes 응답 그대로 GET …/routes/{id}/board 응답 재구성 GET …/routes/{id}/vehicles 응답 그대로 // info.version: 2.0.0 (파괴적 변경) // 새 엔드포인트는 없다. 경로도 그대로다 // /routes · /vehicles 는 스키마가 // 한 필드도 안 바뀐다
/routes와 /vehicles는 응답 스키마가 한 필드도 바뀌지 않는다.
그쪽 화면 코드는 손대지 않아도 된다. 예외가 하나 있는데 /vehicles의 폴링 방식이고,
그것도 코드를 줄이는 쪽이다(6절).
그래도 파괴적 변경이다. 같은 경로 /api/v1/…/board가 다른 모양의 JSON을 준다.
v3 코드로 v4 응답을 읽으면 stops[].seatForecast가 undefined가 되고
forecast.validUntil이 없어 폴링이 멈춘다. 조용히 빈 화면이 되는 종류의 고장이라
배포 순서를 FE·BE가 함께 정해야 한다. 두 판을 한동안 같이 서빙할지는 아직 정해지지 않았다.
화면 골격은 그대로다 — 정류장 목록·이름·방향, 회차 판정(turnSequence),
개편 감지(referenceVersionId)는 v3과 같은 필드다.
고칠 곳은 정류장 한 줄을 그리는 컴포넌트와 그 위의 응답 타입이다.
1답하는 질문이 바뀐다 — 이것이 나머지 전부의 뿌리다
질문 "이 시간대에 여기 지나는 버스라면 앉을 수 있나" 답 범계역 08~09시 → 83.2% // 입력 셋 — 노선 · 정류장 · 시간대 // 버스를 특정하지 않는다 // 한 시간 동안 같은 값이다
질문 "지금 오는 이 버스가 여기 도착할 때 앉을 수 있나" 답 범계역 · 3정류장 앞 204000262 → 87.1% 범계역 · 12정류장 앞 204000271 → 94.6% // 입력에 그 차량의 현재 잔여석이 더해진다 // 버스를 특정한다 (vehicleId) // 관측이 들어올 때마다 값이 바뀐다
사용자가 정류장에서 실제로 하는 질문은 "지금 오는 저 버스에 앉을 수 있나"다. 새 모델은 그 차량의 현재 잔여석을 조건으로 받으므로 버스를 특정해야 답이 나온다. 응답에서 값이 정류장이 아니라 차량에 붙게 되는 이유가 여기 있고, 아래 2~7절은 전부 이 한 문장의 결과다.
확률의 뜻은 v3과 같다 — 도착하는 순간 빈자리가 한 자리 이상 남을 모델 확률이고,
0~1 실수를 그대로 준다. 서버는 반올림하지 않는다(0.996이 1.00이 되면
"반드시 자리가 있다"로 읽힌다). 표시 자릿수는 화면이 정한다.
차량이 특정된다는 점만 다르다. 정류장에 몇 명이 서 있는지는 모르므로
여전히 내가 탈 수 있을 확률은 아니다.
v3이 주던 시간대 평균은 폴백으로도 남지 않는다. 새 모델이 그 값을 이미 재료로 쓰기 때문이다 — 정류장·시간대별 평균에 그 버스의 현재 잔여석을 더해 답을 낸다. 따로 내보내면 덜 가공한 재료를 내보내는 셈이 된다.
v4-fe Client API에서 보기 →응답이 어떻게 달라지나
2/board 응답 구조 — 값 하나에서 배열로
"stops": [
{ "sequence": 9,
"stationId": "228000651",
"name": "범계역",
"direction": "UP",
"boardingAllowed": true,
"seatForecast": {
"kind": "ESTIMATED",
"seatAvailableProbability": 0.832 } },
{ "sequence": 11,
"name": "인덕원역(경유)",
"boardingAllowed": false,
"seatForecast": {
"kind": "NOT_APPLICABLE",
"reason": "BOARDING_NOT_ALLOWED" } } ]
// 승차 가능 정류장은 늘 값을 하나 가졌다
// 승차 불가는 판별자로 자리만 채웠다"stops": [
{ "sequence": 55,
"stationId": "209000135",
"name": "롯데백화점.범계역",
"direction": "DOWN",
"boardingAllowed": true,
"arrivals": [
{ "vehicleId": "204000262",
"horizonStops": 3,
"seatAvailableProbability": 0.871,
"expectedSeats": 28.2 },
{ "vehicleId": "204000271",
"horizonStops": 12,
"seatAvailableProbability": 0.946,
"expectedSeats": 33.1 } ] },
{ "sequence": 62,
"name": "학의JC(경유)",
"boardingAllowed": false,
"arrivals": [] } ]
// 골격 다섯 필드는 그대로다
// 마지막 하나만 배열이 된다 (maxItems 2)
// 정렬은 서버 고정 — horizonStops 오름차순정류장의 골격 다섯(sequence·stationId·name·direction·boardingAllowed)은
v3 그대로다. 여섯 번째 필수 필드만 seatForecast(객체 하나)에서 arrivals(배열)로 바뀐다.
낱말 둘을 먼저 정한다.
horizonStops는 시간이 아니라 정류장 수다 — 그 버스가 대상 정류장까지 몇 칸 남았는가이고 1~12다.
expectedSeats는 선택 필드로, 도착 시 잔여좌석의 기댓값이다.
모델이 좌석 분포를 내므로 "몇 석 남아서 올지"까지 쓸 수 있다.
horizonStops를 분으로 환산하지 마라. 정류장 수와 시간은 다른 축이다 —
같은 8정류장이 10%분위 9.1분에서 90%분위 23.0분까지 흩어지고, 거꾸로 "5~10분" 구간에는
2·3·4·6·8정류장이 모두 섞인다. "몇 분 후 도착"은 v3도 주지 않았고 v4도 주지 않는다
(도착정보 API를 수집하지 않는다).
이 응답 하나로 노선 화면이 완성된다.
sequence − horizonStops가 그 차량이 지금 있는 순번이라 노선 위에 버스를 찍을 수 있고,
하행 정류장에서 그 값이 route.turnSequence 이하이면 회차를 지나서 오는 버스다
(실측으로 arrivals 항목의 10.0%(1650)·12.8%(3330)가 여기 해당한다).
같은 vehicleId가 여러 정류장에 나타나면 전부 같은 차 한 대다.
차량의 실제 잔여석이 필요하면 /vehicles를 함께 부르고 vehicleId로 맞춘다.
3사라지는 것 — 다섯
{
"route": { … },
"forecast": {
"publicationId": "pub-01J9X2",
"targetAt": "…T08:00:00+09:00",
"publishedAt": "…T08:00:02+09:00",
"validUntil": "…T09:00:00+09:00",
"model": { "releaseId": …,
"trainedThrough": … } },
"stops": [ … ] }
// 503 사유 5종 가운데 하나
// NO_CURRENT_PUBLICATION{
"route": { … },
"observedAt": "2026-08-18T08:29:47+09:00",
"model": { "releaseId": …,
"trainedThrough": … },
"vehiclesInService": 14,
"stops": [ … ] }
// model 은 필드가 그대로다.
// 감싸던 forecast 가 사라져 자리만 올라왔다
// 503 사유는 여전히 5종이고 이름 하나만 바뀐다
// NO_RECENT_OBSERVATION| 사라지는 것 | 왜 | 대신 무엇을 읽나 |
|---|---|---|
forecast( ForecastMeta) |
시간대 경계마다 한 판을 구워 봉인하던 구조를 전제한 값이다. 새 예보는 관측마다 바뀌므로 굽는 판이 없다 | observedAt 하나 |
publicationId |
같은 판인지 가려내는 값이었다. 판이라는 것이 없어졌다 | — |
targetAt |
"이 시간대를 가정하고 구웠다"는 기준 시각이다. 가정이 아니라 실제 관측 시각을 쓰게 됐다 | observedAt |
publishedAt · validUntil |
발행 시각과 만료 시각이다. validUntil로 다음 요청 시각을 잡던 코드가 갈 곳을 잃는다 |
Cache-Control(6절) |
stops[].seatForecast |
예보가 정류장이 아니라 차량에 붙으므로 정류장당 값 하나라는 자리가 성립하지 않는다 | stops[].arrivals[] |
kind 판별자( ESTIMATED) |
갈래가 하나만 남아 판별할 것이 없다. 두 필드를 감싸기만 하는 껍데기도 함께 없앴다 | arrivals[] 항목에 필드를 바로 둔다 |
NOT_APPLICABLE( reason: BOARDING_NOT_ALLOWED) |
승차 불가 정류장은 빈 배열로 뜻이 전달된다. 판별자를 지키려고 껍데기를 남길 이유가 없다 | boardingAllowed: false와 arrivals: [] |
NO_CURRENT_PUBLICATION503 사유 |
없는 것이 "유효한 발행본"에서 "최근 관측"으로 바뀐다 | NO_RECENT_OBSERVATION |
오류 규칙 하나도 함께 사라진다. v3은 정류장 하나라도 점수화에 실패하면 per-stop 사유를 섞지 않고 발행 전체를 503으로 되돌렸다. v4에서는 예보가 차량에 붙으므로 그 규칙이 적용되지 않는다 — 예보를 못 내는 항목은 그 항목만 빠지고 응답은 200이다. 503은 노선 자체를 서빙할 수 없을 때만 낸다.
여기에 화면에서 보이지 않는 구멍이 하나 생긴다. 잔여석이 빠진 차량은
arrivals에서 조용히 사라져 "그 자리에 버스가 없다"와 구별되지 않는다.
드물지만 0은 아니다 — 상류가 잔여석을 빠뜨린 경우가 전수 314,688 관측행 중 237행(0.075%)이고,
스냅샷의 1.07%가 그런 차량을 하나 이상 담으며 한 차량이 연속 18회까지 이어진 적이 있다.
결측을 화면에서 가려야 한다면 /vehicles를 함께 읽어 seat.kind = UNKNOWN으로 확인한다.
4생기는 것 — 최상위 둘 · 배열 하나 · 방향 정보 넷
"route": {
"id": "204000057",
"displayName": "3330",
"startStopName": "도촌동9단지앞",
"endStopName": "안양역",
"status": "FORECAST_READY",
"turnSequence": 43,
"referenceVersionId": "3330-v5",
"directions": [
{ "id": "UP", "name": "안양역 방면" },
{ "id": "DOWN", "name": "도촌동9단지앞 방면" } ] }
// 운행 종료를 판단할 근거가 응답에 없었다
// 운행 차량 수도 주지 않았다"route": {
앞의 일곱 필드는 그대로
"directions": [
{ "id": "UP", "name": "안양역 방면",
"originStopName": "도촌동9단지앞",
"terminalStopName": "안양역",
"firstDepartureTime": "04:50",
"lastDepartureTime": "23:30" },
{ "id": "DOWN", "name": "도촌동9단지앞 방면",
"originStopName": "안양역", 회차 정류장
"terminalStopName": "도촌동9단지앞",
"firstDepartureTime": "05:00",
"lastDepartureTime": "23:30" } ] }
// route 여덟 필드 중 모양이 바뀌는 곳은
// directions 하나뿐이다| 새 필드 | 타입 · 값 | 무엇에 쓰나 |
|---|---|---|
observedAtBoard 필수 |
date-time | 이 예보가 보고 낸 차량 관측의 시각이다. 이미 지나간 시각이고 도착 예정 시각이 아니다.
지금 − observedAt이 이 자료의 나이이고, 그 나이로 상한 응답을 거른다 |
vehiclesInServiceBoard 필수 |
integer, 0 이상 | observedAt 시점에 이 노선을 도는 것으로 관측된 차량 수.
arrivals가 빈 이유를 가르는 값이다(5절).
관측이지 단정이 아니다 |
stops[].arrivalsStopState 필수 |
배열, maxItems: 2 |
이 정류장에 다음으로 도착할 버스 최대 두 대. 가까운 순으로 서버가 정렬을 고정한다. 빈 배열이 정상 응답이다 |
arrivals[].vehicleId |
string | null, 필수 |
공급자(GBIS) 차량 ID. /vehicles의 같은 필드와 같은 값이라 두 응답을 잇는다.
필수 필드지만 공급자가 빠뜨리는 드문 경우가 있어 null이 온다 — 타입을 그렇게 잡는다 |
arrivals[].horizonStops |
integer, 1~12, 필수 | 그 버스가 몇 정류장 앞에 있는가. sequence − horizonStops가 그 차량의 현재 순번이다.
화면에서 감추지 마라(5절) |
arrivals[].seatAvailableProbability |
number, 0~1, 필수 | 이 버스가 이 정류장에 도착할 때 빈자리가 한 자리 이상 남을 모델 확률. 뜻은 v3과 같고 차량이 특정된다는 점만 다르다 |
arrivals[].expectedSeats |
number, 0 이상, 선택 | 도착 시 잔여좌석의 기댓값. "몇 석 남아서 올지"를 쓰려면 이 값을 읽는다. 선택이므로 없을 때의 화면을 정해 두어야 한다 |
directions[].originStopNamedirections[].terminalStopName |
string, 필수 | 구간을 회차 지점이 가른다 — UP은 (노선 기점 → 회차 정류장),
DOWN은 (회차 정류장 → 노선 종점)이다.
DOWN의 originStopName이 그 방향 첫 정류장이 아니라 회차 정류장인 이유는,
타는 사람에게 "안양역에서 출발해 도촌동9단지앞으로 간다"가 맞는 말이기 때문이다 |
directions[].firstDepartureTimedirections[].lastDepartureTime |
string HH:MM(KST), 필수 |
"운행이 끝났습니다"를 vehiclesInService만으로 단정하지 않기 위한 권위 근거다.
방향마다 다르다 — 1650은 상행 막차 22:35, 하행 23:55로 80분 차이가 난다 |
NO_RECENT_OBSERVATION503 사유 |
enum 값 | 최근 차량 관측이 없어 어떤 예보도 기준 시각을 가질 수 없다.
v3에 없던 결합이다 — v3은 수집이 끊겨도 예보가 유효했지만 v4의 /board는 관측을 읽는다 |
이름이 남지만 뜻이 옮겨가는 것이 하나 있다.
RouteStatus는 값 2종(FORECAST_READY·PREPARING)이 그대로인데,
FORECAST_READY의 뜻이 "지금 시각에 유효한 봉인 발행본이 있다"에서
"활성 계수 묶음이 있어 관측이 들어오면 예보를 낸다"로 바뀐다.
즉 "답을 갖고 있다"에서 "답을 낼 수 있다"로 옮겨간다.
그래서 FORECAST_READY인데도 arrivals가 빌 수 있다 — 오는 버스가 없으면 낼 답이 없다.
이 둘을 섞으면 정상 상태를 오류로 그린다.
화면과 폴링
5화면이 달라지는 곳 — 한 줄에 최대 두 대
범계역 83.2% 평촌역 59.0% 인덕원역(경유) 승차 불가 // 값이 없는 자리는 없었다 // 승차 가능 정류장은 늘 채워졌다 // 정류장 = 값 1:1 이라 한 줄이면 됐다
롯데백화점.범계역 3정류장 앞 87.1% 28.2석 예상 12정류장 앞 94.6% 33.1석 예상 문화의거리 4정류장 앞 84.4% 26.9석 예상 도촌초등학교 (빈 배열 — 문구는 아래 표로 고른다) // 정류장 = 값 1:N 이 된다 (N은 0·1·2) // 0 인 경우가 흔하고 정상이다
고칠 컴포넌트는 정류장 한 줄이다. 값 하나를 그리던 자리가 0~2개의 반복이 된다.
정렬은 서버가 horizonStops 오름차순으로 고정하므로 화면이 다시 정렬할 필요는 없다.
두 대로 자른 것은 상한이 없으면 지평 12 안에 최대 12대까지 담기고 정류장의 11.2%가 네 대 이상을 담기 때문이다.
세 대째는 앞에 버스가 둘이라 대기가 중앙 27분이고 그 사람이 탈 차가 아니다.
두 대로 자르면 정류장의 73.7%는 원래대로다.
horizonStops를 감추고 확률만 띄우면 사용자가 과신한다.
같은 확률이라도 2정류장 전과 8정류장 전은 신뢰도가 다르다 — 만석 버스를 골라내는 능력이
아래처럼 떨어진다. 먼 쪽도 무작위보다는 훨씬 낫지만 같은 숫자로 읽히면 안 된다.
| 남은 정류장 | 만석 버스를 골라내는 능력 | 무작위 대비 |
|---|---|---|
| 1–4 | 0.73~0.84 | 43~50배 |
| 5–8 | 0.54~0.72 | 31~43배 |
| 9–12 | 0.42~0.54 | 24~31배 |
정렬 순서를 도착 순서라고 단정하지도 마라. 거리로 정렬한 것이고 버스 뭉침 때문에 2대 이상일 때 7.87%가 어긋난다. 다만 맨 앞 항목이 실제 첫 도착인 비율이 96.68%이고 두 대로 자르면 그 둘의 순서가 96.76% 맞는다. 가까울수록 정확해서 1정류장 앞이면 96.6%, 10정류장 앞이면 88.5%다.
arrivals: []는 오류가 아니다. 스피너나 오류 화면을 띄우면 안 된다.
운행 시간대(06~23시) 실측 커버율이 90.4%(1650)·91.1%(3330)이니
열 번에 한 번쯤은 빈 배열이 온다. 문구는 아래 넷으로 갈라 고른다 —
vehiclesInService 하나로는 갈라지지 않으므로
sequence와 boardingAllowed를 함께 읽는다.
| 조건 | 뜻 | 화면 |
|---|---|---|
boardingAllowed: false |
여기서는 못 탄다(경유·미정차) | 승차 불가로 표시하고 예보 자리를 비운다 |
vehiclesInService: 0 |
노선에 도는 차가 없다 | lastDepartureTime과 견주고 나서 "운행이 끝났습니다".
방향마다 막차가 다르므로 노선을 통째로 종료라고 하면 틀린다 |
sequence ≤ 12 |
노선 앞머리라 뒤로 볼 칸이 모자란다 | "곧 오는 버스 없음"이라고 쓰면 거짓이다 |
| 그 밖 | 이 구간에 차가 없다 | "곧 오는 버스 없음" — 기다리면 온다 |
셋째 줄이 함정이다. 순번 12 이하 정류장은 뒤로 볼 수 있는 칸이
sequence − 1개뿐이라 커버율이 37~44%로 떨어진다(그 밖의 정류장은 98.7~98.9%다).
그런데 실제 다음 버스는 중앙 8.3~12.0분 뒤에 온다 —
그 차가 차고지에서 대기 중이라 차량위치 API에 아직 안 나타날 뿐이다(96.2~96.8%가 그렇다).
해당 정류장은 노선의 13.8%다. 여기서는 배차 간격(첨두 5~6분·비첨두 15분)을 함께 보여주는 편이 정직하다.
vehiclesInService: 0도 관측일 뿐 단정이 아니다.
드물게 상류가 빈 응답을 주면 운행 중에도 0이 된다(실측 22,010 스냅샷 중 1건, 0.005%).
observedAt의 나이로 거른다.
6폴링 — 주기를 프론트가 계산하지 않는다
/board Cache-Control: max-age=60
forecast.validUntil 로 다음 판을 안다
07 · 09 · 17 · 20 KST 경계마다 한 판
/vehicles Cache-Control: max-age=15
설명은 "60초면 충분하다"
→ 헤더와 권고가 어긋나 있었다
/routes Cache-Control: max-age=300/board Cache-Control: 서버가 정한다 예: public, max-age=15 /vehicles Cache-Control: 서버가 정한다 예: public, max-age=15 /routes Cache-Control: max-age=300 그대로 // max-age 는 고정값이 아니다. // 서버가 그 시점의 수집 주기에 맞춰 낸다 // Cache-Control 만 따르면 된다 // 값이 바뀌는 횟수 하루 4번 → 관측마다 // 08-18 실측 노선당 4,097회
프론트가 폴링 주기를 계산하지 않는다. 무엇이 급한지는 서버가 이미 알고
캐시 수명에 담았다. validUntil로 다음 요청 시각을 잡던 코드도, 화면 상태마다
간격을 다르게 두던 코드도 지운다. 수집 주기보다 자주 불러도 같은 값이 온다 —
새 판이 나오지 않았으면 바뀔 것이 없다.
/vehicles도 함께 바뀐다. v3은 max-age=15 고정이었는데
(설명 문구는 60초를 권해 서로 어긋나 있었다) v4는 /board와 같은 값을 따른다.
수집이 적응형이라 시간대마다 다르다.
| 수집 단계 | 시각대(KST) | max-age |
|---|---|---|
| 첨두 | 07–08 · 17–20 | 15 |
| 늦은 저녁 | 20–23 | 15 |
| 새벽·심야 경계 | 00 · 04–06 · 23 | 20 |
| 낮 | 09–15 | 60 |
| 한산 | 16 | 240 |
| 심야 | 01–03 | 600 |
자료의 나이는 지금 − observedAt으로 잰다. 배경으로 열려만 있는 화면은 부르지 않는다.
오류 응답(4xx·5xx)은 v3과 같이 Cache-Control: no-store이고,
재시도 시각을 아는 retryable 503에만 Retry-After가 붙는다.
옮겨가는 순서
7마이그레이션 체크리스트 — 타입 · 렌더링 · 빈 상태 · 폴링
type SeatForecast =
| { kind: "ESTIMATED";
seatAvailableProbability: number }
| { kind: "NOT_APPLICABLE";
reason: "BOARDING_NOT_ALLOWED" }
interface Stop {
sequence: number
stationId: string
name: string
direction: "UP" | "DOWN"
boardingAllowed: boolean
seatForecast: SeatForecast
}
interface Board {
route: BoardRoute
forecast: ForecastMeta
stops: Stop[]
}interface StopArrival { vehicleId: string | null 필수 · null 온다 horizonStops: number 필수 · 1~12 seatAvailableProbability: number 필수 expectedSeats?: number 선택 } interface Stop { sequence: number stationId: string name: string direction: "UP" | "DOWN" boardingAllowed: boolean arrivals: StopArrival[] 최대 2 · 빈 배열 정상 } interface Board { route: BoardRoute observedAt: string model: ModelInfo 최상위로 올라옴 vehiclesInService: number stops: Stop[] }
| 순서 | 고치는 것 | 다 됐다고 보는 기준 |
|---|---|---|
| 1 타입 정의 | SeatForecast 유니온과 ForecastMeta를 지운다.
StopArrival을 만들고 Stop.arrivals·Board.observedAt·Board.model·Board.vehiclesInService를 넣는다.
DirectionInfo에 필수 넷을 더한다.
vehicleId는 string | null, expectedSeats는 선택으로 잡는다.
503 사유 문자열을 NO_RECENT_OBSERVATION으로 바꾼다 |
타입 검사만으로 고쳐야 할 자리가 전부 빨갛게 뜬다. 그 목록이 곧 2~4번의 작업 범위다 |
| 2 렌더링 | 정류장 한 줄이 값 하나가 아니라 0~2개의 반복이 된다.
각 줄에 horizonStops를 함께 그린다.
expectedSeats가 없을 때의 모양을 정한다.
sequence − horizonStops로 노선 위에 버스를 찍고, 하행에서 그 값이
turnSequence 이하이면 회차를 지나 오는 차량으로 표시한다.
서버 정렬(horizonStops 오름차순)을 그대로 쓴다 |
한 정류장에 두 대가 실린 응답으로 화면이 겹치지 않는다.
확률을 반올림해 100%로 띄우는 자리가 없다 |
| 3 빈 상태 처리 | arrivals: []에 스피너·오류를 띄우지 않는다.
5절 표의 네 갈래로 문구를 고른다.
"운행이 끝났습니다"는 vehiclesInService: 0과
해당 방향의 lastDepartureTime을 함께 보고 쓴다.
FORECAST_READY인데 arrivals가 빈 것을 오류로 그리지 않는다 |
운행 시간대의 빈 배열(10%가량)과 심야 응답에서 화면이 깨지지 않는다. 순번 12 이하 정류장에 "곧 오는 버스 없음"이 뜨지 않는다 |
| 4 폴링 로직 | validUntil로 다음 요청 시각을 잡던 코드를 지운다.
화면 상태별 간격 계산도 지운다. Cache-Control만 따른다.
/vehicles도 고정 간격을 걷어낸다.
자료의 나이는 지금 − observedAt으로 잰다 |
폴링 주기를 정하는 상수가 프론트 코드에 남아 있지 않다 |
목업·픽스처·테스트 상수도 함께 본다. /board 응답 구조가 통째로 바뀌므로
v3 응답을 베낀 고정 자료는 전부 다시 써야 한다.
그때 노선 ID도 확인한다 — 204000057이 3330, 234000050이 1650이고
turnSequence는 3330이 43, 1650이 44다.
v3 초판 예시에 있던 234000016·turnSequence: 31·3330의 기점 "구리수택차고지"는
셋 다 틀린 값이었다 — 상류 API로 직접 조회해 2026-08-19에 위 값으로 교정했다.
구리수택차고지는 1650의 기점이고 나머지 둘은 어느 노선의 값도 아니다.
그 값을 베낀 자리가 있으면 함께 고친다.
FE와 BE가 함께 정해야 하는 것이 둘 남아 있다.
NO_RECENT_OBSERVATION을 내는 문턱을 /vehicles의 staleAt과
같은 값으로 묶을지(다르면 화면이 두 말을 한다), 그리고 전환 절차 —
경로는 /api/v1 그대로인데 응답 모양이 바뀌므로 두 판을 한동안 함께 서빙할지
같은 날 함께 올릴지가 정해지지 않았다.
이 문서는 검토 요청안이다. 확정 계약은 여전히 v3이고,
채택하지 않기로 하면 v3이 그대로 남는다. 다만 채택하면 프론트엔드 수정이 필요하고
되돌림도 계약 되돌림이 된다 — /board를 v3 형태로 되돌리는 것 말고는 물러설 곳이 없다.
필드 하나하나의 정확한 뜻과 예시는 v4-fe Client API에 있고,
백엔드 쪽 변경은 BE 변경 이력에 있다.
모델 자체는 08번 「오고 있는 버스 보기」에 있다.