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>
53 lines
2.7 KiB
TypeScript
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(' / ')}`);
|
|
});
|
|
});
|