프론트엔드 변경 이력 — v4 → v4-1

프론트엔드 크루가 읽는 문서다. v4에서 v4-1로 넘어가며 화면과 코드가 실제로 바뀌는 것만 담았다. 이전 개정(v3 → v4, 2026-08-19)의 이력은 전체 변경 이력에 있다. 백엔드 쪽 변경은 BE 변경 이력에 따로 있다.

2026-08-31 — 계약 v4-1 (2.1.0, 확정)

1오류 응답에서 retryable 이 빠졌다 — 3필드

오류 응답은 code · message · requestId 셋이다. 재시도 여부는 응답에 담지 않는다 — 상태 코드와 Retry-After 유무로 판단한다.

  {
    "code": "ROUTE_NOT_FOUND",
    "message": "등록되지 않은 노선입니다.",
-   "requestId": "req-01J9X2ABCE",
-   "retryable": false
+   "requestId": "req-01J9X2ABCE"
  }

깨지는 변경이다. retryable 을 읽는 코드는 지우고, 재시도 판단은 이렇게 바꾼다. 값이 상태 코드에서 유도되는 중복이라 응답에서 뺐다.

- if (error.retryable) retry();
+ if (res.status === 503 && res.headers.has("Retry-After")) retry();

2오류 코드 5종 → 9종

전에는 없는 경로·틀린 메서드·미처리 예외가 계약 밖 모양(프레임워크 기본 응답)으로 샜다. 이제 무엇이 와도 위 3필드 한 모양으로 온다.

  INVALID_ROUTE_ID        400
  ROUTE_NOT_FOUND         404
  MODEL_OUT_OF_SCOPE      503
  NO_RECENT_OBSERVATION   503
  SERVICE_UNAVAILABLE     503
+ INVALID_REQUEST         400   경로 파라미터 밖의 형식 오류
+ ENDPOINT_NOT_FOUND      404   없는 경로 (예: GET /api/v1/route — s 빠짐)
+ METHOD_NOT_ALLOWED      405   지원하지 않는 메서드 (예: POST /api/v1/routes)
+ INTERNAL_ERROR          500   서버가 요청을 처리하지 못함

추가만이라 무해하다. ErrorCode 타입을 닫아 둔 클라이언트만 네 값을 더한다. 화면 분기가 필요 없는 코드는 공통 오류 화면으로 보내면 된다.

3보드 응답에 staleAt 추가

전에는 "언제부터 낡은 보드인가"의 기준(관측 + 5분)이 서버 안에만 있었다. 이제 응답이 직접 말한다. /vehicles 의 observation.staleAt 과 같은 이름, 같은 뜻이다.

  {
    "observedAt": "2026-08-31T13:00:00+09:00",
+   "staleAt":    "2026-08-31T13:05:00+09:00",
    "model": { … },

추가만이라 무해하다. 5분을 하드코딩했다면 이 값으로 바꾼다 — 정책이 바뀌어도 화면 수정이 없다. (SAL-113)

+ const stale = Date.now() > Date.parse(board.staleAt);   // true 면 낡음 표시

4observation.state — 읽어낸 관측이 0행이면 UNKNOWN

상류가 차량 행을 줬는데 서버가 하나도 못 읽어낸 경우의 판정이 갈렸다.

  상황: 상류는 차량 행을 줬는데, 서버가 한 행도 못 읽었다

- "observation": { "state": "NO_VEHICLES_OBSERVED", … }   ← "차가 없다"로 보인다
+ "observation": { "state": "UNKNOWN", … }                ← "모른다"가 정직하다

상류가 필드 형식을 바꾸면 그때부터 모든 행이 걸러지는데, 그것이 "0대 운행"으로 보이면 화면도 운영도 이상을 알아챌 수 없다. enum 은 그대로다 — UNKNOWN 화면 처리가 이미 있으면 할 일이 없다.

5sequence − horizonStops 는 현재 순번이 아니다 — 본문 아홉 곳 정정

horizonStops 의 정의부터 정확히 하면 — 목표 정류장 순번에서, 그 버스가 지나온 정류장 순번(통과 순번)을 뺀 값이다. 버스가 어느 정류장에 도착 중(phase: ARRIVING)이면 그 정류장은 아직 안 지난 것이라, 통과 순번은 현재 순번보다 1 작다. 여기서 어긋남이 생긴다.

 순번:      5      6      7      8      9      10
 노선:  ---o------o------B------o------o------G--->

        B = 버스 (7번에 도착 중, phase ARRIVING)
        G = 목표 정류장 (sequence 10)

 지나온 정류장(통과 순번) = 6         7번에는 "도착 중"이라 아직 안 지났다
 horizonStops = 10 − 6 = 4           앞으로 지날 곳: 7 · 8 · 9 · 10 네 칸

 옛 본문의 역산:   10 − 4 = 6        ← 6은 "지나온" 정류장이다
 버스의 실제 위치: 7                  ← 지도에 그리면 한 칸 뒤에 찍힌다

같은 버스라도 상태에 따라 갈린다 — 어긋나는 것은 도착 중일 때뿐이다.

버스 상태 (7번 정류장 기준)currentStopSequence통과 순번horizonStops
(목표 10)
10 − horizonStops실제 위치와
7번에 도착 중 ARRIVING76461 작다
7번에서 출발 DEPARTED7737일치
7→8 이동 중 IN_TRANSIT7737일치

이 1칸이 제일 아픈 곳이 회차 판정이다. 경계에서 방향이 뒤집힌다.

 회차 순번(route.turnSequence) = 43
 버스: 44번 정류장에 도착 중  →  실제로는 회차를 이미 지났다 (44 > 43)

- 옛 판정:  55(목표 sequence) − 12(horizonStops) = 43  ≤ 43  →  "아직 회차 전"   ← 오판
+ 새 판정:  /vehicles 의 currentStopSequence 44  >  43        →  "회차 지남"     ← 맞다

그래서 본문 아홉 곳을 이렇게 바로잡았다.

- `sequence − horizonStops` 가 그 차량의 현재 순번이다
+ `sequence − horizonStops` 는 그 차량이 지나온 정류장 순번이다.
+ 도착 중인 차량은 현재 순번보다 1 작다.
+ 버스 위치·회차 판정은 /vehicles 의 currentStopSequence 로 한다.

스키마(minimum 1 · maximum 12)와 값은 그대로다 — 서버가 주는 숫자는 전에도 지금도 옳았고, 틀린 것은 그 숫자를 읽는 법을 적은 본문이었다. 본문대로 역산해 지도를 그렸다면 도착 차량 1칸 오차와 회차 오판이 이번 정정으로 사라진다. 두 응답은 vehicleId 가 같은 값이라 그대로 이어 붙일 수 있다.

6info.version 2.0.0 → 2.1.0

  info:
-   version: 2.0.0
+   version: 2.1.0

1·2·3이 스키마 변경이라 마이너를 올렸다.

전체 변경 이력 →

이 페이지는 최신 개정 하나만 담는다. 지난 개정 이력(v3 → v4 · 2026-08-19 팀 검토 반영 · 데모 재적합)은 전체 변경 이력으로 옮겼다.