Files
homeclaw/tests/kma-grid-table.test.ts
T
kimandClaude Opus 5 4c0e19a8ff fix: KMA_GRID 격자 3건 수정 — 구미는 45km 떨어진 산간을 읽고 있었다
ASOS 승인으로 관측 실측이 생겨서, 그걸 정답지로 KMA_GRID 표를
전수 감사했다. 검증 가능한 27개 도시 중 23개는 Δ2.1°C 이내였고
3건이 틀렸다.

  구미  [75,100] → [84,96]   ASOS 31.5°C인데 표는 25.3°C(Δ6.2).
                             45km 떨어진 산간 격자였다. 변환값은 Δ0.4.
  거제  [95,71]  → [90,69]   T1H가 기상청 결측 표식 -999. 자료가 아예
                             없는 격자였다. 변환값은 Δ0.2.
  시흥  [57,120] → [57,123]  안산 [58,121]보다 남쪽에 놓여 있었는데
                             시흥시(37.38°N)는 안산시(37.32°N)보다
                             북쪽이다. ASOS 지점이 없어 실측 대조는
                             못 하고(두 후보 다 값은 그럴듯했다 —
                             해안 23.9°C/94% vs 내륙 30.2°C/67%)
                             인접 도시와의 남북 순서로 판정했다.

인천(Δ4.5)·창원(Δ3.2)도 검사에 걸렸지만 표와 좌표 변환값이 동일해
손대지 않았다 — 격자 오류가 아니라 관측소 위치와 격자 중심의 차이다.

재발 방지로 KMA_GRID ↔ KR_CITY_COORDS 일치 테스트를 걸었다. 둘은
같은 도시를 격자와 좌표로 각각 적어둔 표라 서로 맞아야 하고, 이
대조는 네트워크 없이 결정적으로 돈다. 실제로 시흥을 이 테스트가 잡았다.

지점 좌표 공식 목록은 못 구했다. data.go.kr의 지점정보 서비스는 REST가
아닌 LINK형이라 엔드포인트가 없고(후보 21개 전부 NO_OPENAPI_SERVICE),
apihub.kma.go.kr의 stn_inf.php는 자체 인증키를 요구해 data.go.kr 키로는
401이다. 그게 생기면 좌표 입력도 최근접 ASOS 지점으로 올릴 수 있다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:22:47 +09:00

53 lines
2.7 KiB
TypeScript

/**
* kma-grid-table.test.ts
*
* KMA_GRID(도시명 → 격자) 표와 KR_CITY_COORDS(도시명 → 좌표) 표는 같은 도시를 두 방식으로
* 적어둔 것이므로, 좌표를 격자로 변환하면 표의 격자와 같아야 한다. 이 불변식이 깨진
* 항목은 둘 중 하나가 틀린 것이다.
*
* 2026-08-16에 이 대조를 처음 돌려서 두 건을 잡았다. ASOS 관측기온을 정답지로 확인한 결과:
* - 구미 표[75,100] → ASOS 31.5°C인데 25.3°C(Δ6.2). 45km 떨어진 산간 격자였다.
* 변환값 [84,96]은 31.9°C(Δ0.4) → 표를 교체.
* - 거제 표[95,71] → T1H가 기상청 결측 표식 -999. 자료가 없는 격자였다.
* 변환값 [90,69]는 30.8°C 대비 31.0°C(Δ0.2) → 표를 교체.
*
* 나머지 25개 도시는 ASOS 실측과 Δ2.1°C 이내로 멀쩡했다. 인천(Δ4.5)·창원(Δ3.2)도 검사에
* 걸렸지만 표와 변환값이 동일했다 — 격자 오류가 아니라 관측소 위치와 격자 중심의 차이라
* 손대지 않았다. 그래서 이 테스트의 기준은 "ASOS와 몇 도 차이냐"가 아니라
* "두 표가 서로 일치하냐"다. 후자는 네트워크 없이 결정적으로 검증된다.
*/
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { KMA_GRID, KR_CITY_COORDS, latLonToKmaGrid } from '../src/tools/weather';
describe('KMA_GRID ↔ KR_CITY_COORDS 일치', () => {
const shared = Object.keys(KMA_GRID).filter(k => KR_CITY_COORDS[k]);
test('대조할 도시가 실제로 있다 (표가 비면 통과해버리는 걸 막는다)', () => {
assert.ok(shared.length >= 20, `공통 도시 ${shared.length}개 — 너무 적다`);
});
for (const city of ['구미', '거제', '서울', '부산', '대전']) {
test(`${city}: 좌표 변환값이 표의 격자와 같다`, () => {
const [lat, lon] = KR_CITY_COORDS[city];
const [nx, ny] = KMA_GRID[city];
assert.deepEqual(latLonToKmaGrid(lat, lon), { nx, ny });
});
}
test('공통 도시 전부: 표와 변환값이 2칸 이내', () => {
// 완전 일치를 요구하면 도시 중심점 정의 차이로 1칸씩 흔들리는 걸 잡아버린다.
// 문제가 됐던 건 구미 13칸, 거제 7칸 수준이라 2칸이면 충분히 걸러진다.
const off: string[] = [];
for (const city of shared) {
const [lat, lon] = KR_CITY_COORDS[city];
const [nx, ny] = KMA_GRID[city];
const g = latLonToKmaGrid(lat, lon);
const d = Math.abs(g.nx - nx) + Math.abs(g.ny - ny);
if (d > 2) off.push(`${city} 표[${nx},${ny}] vs 변환[${g.nx},${g.ny}] 거리${d}`);
}
assert.deepEqual(off, [], `격자가 어긋난 도시: ${off.join(' / ')}`);
});
});