feat: ASOS 지점 좌표를 공식 목록으로 교체 + 좌표 입력도 관측값 사용
API허브 지점정보(stn_inf.php?inf=SFC)가 승인돼서 97개 지점의 공식 위경도를 받았다. 이걸로 nearestAsosStation을 붙여, 이름으로 못 맞히는 위치(좌표 입력, ASOS 없는 소도시)도 40km 이내 최근접 관측소를 쓴다. 이제 좌표든 이름이든 전부 기상청 실측이고, Open-Meteo 추정은 40km 안에 관측소가 없을 때(독도·먼바다)만 나온다. 같은 서울을 이름과 좌표로 물었을 때 사상 누적이 갈리던 비대칭도 이걸로 사라졌다. 최근접으로 잡은 경우엔 "기상청 산청 관측 (13.6km)"처럼 거리를 같이 내보내, 그 지점이 대표성이 있는지는 사용자가 판단하게 둔다. 앞서 지점명 지오코딩으로 좌표를 만들려던 시도가 왜 틀렸는지도 공식 좌표로 확인됐다 — 홍성·보령·밀양이 북한 동명 지역, 남원이 제주도, 남해가 충청으로 잡혔던 것들이 전부 제자리를 찾았다. 지점 표는 정적으로 박았다. 관측소 좌표는 거의 안 바뀌는데 런타임에 API허브를 타면 별도 키에 매 요청이 묶인다. 갱신은 주석의 URL을 다시 받아 표만 갈아끼우면 된다. apiHubFetch/getKmaApiHubKey를 같이 넣었다. 키는 vault의 kma.apihub_key (config.json 평문 아님, data.go.kr 키와 별개 계정). 인코딩이 응답 종류마다 달라서 여기 가둬뒀다 — 자료 본문은 EUC-KR인데 에러는 UTF-8 JSON이라, 에러까지 EUC-KR로 읽으면 "활용신청이 필요합니다"가 깨져 원인을 못 읽는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { findAsosStation, normalizeAsosHour } from '../src/tools/weather';
|
||||
import { findAsosStation, nearestAsosStation, normalizeAsosHour } from '../src/tools/weather';
|
||||
|
||||
describe('findAsosStation — 지점명 매칭', () => {
|
||||
test('정확히 일치하는 지점', () => {
|
||||
@@ -47,6 +47,49 @@ describe('findAsosStation — 지점명 매칭', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('nearestAsosStation — 좌표에서 최근접 지점', () => {
|
||||
// 좌표는 2026-08-16에 기상청 API허브 stn_inf.php(공식)로 교체했다. 그 전엔 지점명을
|
||||
// 지오코딩해 만들려다 홍성·보령·밀양이 북한, 남원이 제주도로 나와서 폐기했었다.
|
||||
test('지점이 바로 있는 도시는 몇 km 안에 잡힌다', () => {
|
||||
const seoul = nearestAsosStation(37.5665, 126.978);
|
||||
assert.equal(seoul?.name, '서울');
|
||||
assert.ok(seoul!.km < 5, `서울 ${seoul!.km}km — 너무 멀다`);
|
||||
|
||||
const gumi = nearestAsosStation(36.1195, 128.3446);
|
||||
assert.equal(gumi?.name, '구미');
|
||||
assert.ok(gumi!.km < 5);
|
||||
});
|
||||
|
||||
test('지점이 없는 도시는 이웃 지점으로 넘어간다', () => {
|
||||
// 시흥·용인엔 ASOS가 없다 — 각각 인천·수원이 가장 가깝다.
|
||||
assert.equal(nearestAsosStation(37.3800, 126.8029)?.name, '인천');
|
||||
assert.equal(nearestAsosStation(37.2411, 127.1776)?.name, '수원');
|
||||
});
|
||||
|
||||
test('공식 좌표라서 지오코더가 틀렸던 지점도 제자리에 있다', () => {
|
||||
// 예전 지오코딩은 남해를 36.44,127.41(충청)로 봤다. 공식은 34.82,127.93.
|
||||
assert.equal(nearestAsosStation(34.8376, 127.8925)?.name, '남해');
|
||||
// 홍성/보령/밀양은 북한 동명 지역으로 튀었던 것들.
|
||||
assert.equal(nearestAsosStation(36.6576, 126.6877)?.name, '홍성');
|
||||
assert.equal(nearestAsosStation(35.4915, 128.7441)?.name, '밀양');
|
||||
});
|
||||
|
||||
test('maxKm 밖이면 null — 먼 관측소를 "관측값"으로 빌려 쓰지 않는다', () => {
|
||||
assert.equal(nearestAsosStation(37.2429, 131.8664), null); // 독도
|
||||
assert.equal(nearestAsosStation(34.3, 129.3), null); // 대마도 근해
|
||||
});
|
||||
|
||||
test('maxKm를 넓히면 같은 좌표도 잡힌다', () => {
|
||||
const far = nearestAsosStation(37.2429, 131.8664, 200);
|
||||
assert.equal(far?.name, '울릉도');
|
||||
});
|
||||
|
||||
test('좌표가 숫자가 아니면 null', () => {
|
||||
assert.equal(nearestAsosStation(NaN, 127), null);
|
||||
assert.equal(nearestAsosStation(37, NaN), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeAsosHour — rn 인코딩', () => {
|
||||
test('""는 무강수 (결측 아님)', () => {
|
||||
assert.deepEqual(normalizeAsosHour('2026-08-15 03:00', ''), { time: '2026-08-15T03:00', mm: 0, wet: false });
|
||||
|
||||
Reference in New Issue
Block a user