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>
This commit is contained in:
+13
-5
@@ -136,7 +136,7 @@ const windDirectionKo = (deg: number) =>
|
||||
|
||||
// Geocode city name → lat/lon using Open-Meteo geocoding (no API key required)
|
||||
// Falls back to a built-in lookup table for common Korean cities.
|
||||
const KR_CITY_COORDS: Record<string, [number, number]> = {
|
||||
export const KR_CITY_COORDS: Record<string, [number, number]> = {
|
||||
'서울': [37.5665, 126.9780], 'seoul': [37.5665, 126.9780],
|
||||
'부산': [35.1796, 129.0756], 'busan': [35.1796, 129.0756],
|
||||
'인천': [37.4563, 126.7052], 'incheon': [37.4563, 126.7052],
|
||||
@@ -308,7 +308,7 @@ function parseCoords(location: string): [number, number] | null {
|
||||
// ── KMA grid coordinate conversion (Lambert Conformal Conic) ─────────────────
|
||||
|
||||
// Pre-computed grid coordinates for major Korean cities (official KMA values)
|
||||
const KMA_GRID: Record<string, [number, number]> = {
|
||||
export const KMA_GRID: Record<string, [number, number]> = {
|
||||
'서울': [60, 127], 'seoul': [60, 127],
|
||||
'부산': [98, 76], 'busan': [98, 76],
|
||||
'인천': [55, 124], 'incheon': [55, 124],
|
||||
@@ -331,16 +331,24 @@ const KMA_GRID: Record<string, [number, number]> = {
|
||||
'진주': [81, 75], 'jinju': [81, 75],
|
||||
'여수': [73, 66], 'yeosu': [73, 66],
|
||||
'목포': [50, 67], 'mokpo': [50, 67],
|
||||
'구미': [75, 100], 'gumi': [75, 100],
|
||||
// 2026-08-16: [75,100]이었는데 45km 떨어진 산간 격자였다. ASOS 구미(279) 실측 31.5°C에
|
||||
// 표는 25.3°C(Δ6.2), 좌표 변환값 [84,96]은 31.9°C(Δ0.4). 표가 틀린 게 확인돼 교체.
|
||||
'구미': [84, 96], 'gumi': [84, 96],
|
||||
'안동': [91, 106], 'andong': [91, 106],
|
||||
'경주': [100, 91], 'gyeongju': [100, 91],
|
||||
'김해': [95, 77], 'gimhae': [95, 77],
|
||||
'거제': [95, 71], 'geoje': [95, 71],
|
||||
// 2026-08-16: [95,71]은 아예 자료가 없는 격자였다 — T1H가 기상청 결측 표식 -999로 왔다.
|
||||
// 좌표 변환값 [90,69]는 ASOS 거제(294) 실측 30.8°C에 31.0°C(Δ0.2).
|
||||
'거제': [90, 69], 'geoje': [90, 69],
|
||||
'순천': [70, 70], 'suncheon': [70, 70],
|
||||
'익산': [60, 91], 'iksan': [60, 91],
|
||||
'원주': [76, 122], 'wonju': [76, 122],
|
||||
'평택': [62, 114], 'pyeongtaek': [62, 114],
|
||||
'시흥': [57, 120], 'siheung': [57, 120],
|
||||
// 2026-08-16: [57,120]이었는데 안산 [58,121]보다 남쪽에 놓여 있었다. 시흥시(37.38°N)는
|
||||
// 안산시(37.32°N)보다 북쪽이라 ny가 더 커야 한다. ASOS 지점이 없어 실측 대조는 못 했고
|
||||
// (두 후보 다 값 자체는 그럴듯했다 — 해안 23.9°C/94% vs 내륙 30.2°C/67%), 인접 도시와의
|
||||
// 남북 순서로 판정했다.
|
||||
'시흥': [57, 123], 'siheung': [57, 123],
|
||||
'부천': [56, 125], 'bucheon': [56, 125],
|
||||
'안산': [57, 121], 'ansan': [57, 121],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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(' / ')}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user