feat: 강수 사상 누적을 ASOS 실측으로 전환 + 격자 변환 +1 밀림 수정
ASOS 시간자료/일자료가 승인돼서, 사상 누적을 Open-Meteo 추정에서 기상청 관측으로 바꾼다. 두 소스의 창이 정확히 맞물린다 — ASOS는 "전날 자료까지"(오늘은 resultCode 99), 초단기실황은 "최근 1일"이라 자정에서 나누면 사상 전체가 관측값이 된다. rn 인코딩 주의: ""=무강수, "0.0"=미량(관측됐으나 0.05mm 미만). 둘을 뭉개면 이슬비로 이어지던 비가 소강으로 잡혀 사상이 끊긴다. 그래서 findRainEpisode가 wet을 명시로 받는다(안 주면 종전 mm 임계). 지점 좌표는 일부러 없다. 최근접 지점 방식을 만들다 폐기했다: 지점명 지오코딩이 홍성·보령·밀양을 북한 동명 지역으로, 남원을 제주도로 돌려줬다. 검증하려고 ASOS 관측기온 vs Open-Meteo 기온을 대조했으나 좌표가 정확한 서울조차 Δ3.1°C(도시열섬·모델편차)라 판별자가 못 됐다. 이름 매칭만 쓰고 못 맞히면 Open-Meteo로 물러난다. 지점 목록 97개는 지점정보 API가 없어서(NO_OPENAPI_SERVICE) 일자료 엔드포인트로 stnIds 90~300을 전수조사해 실응답에서 뽑았다. 같이 고친 것: latLonToKmaGrid가 공식 변환식의 반올림 항을 +0.5가 아닌 +1.5로 써서 nx·ny를 나란히 한 칸씩(대각 ~7km) 밀어 조회했다. 이름이 KMA_GRID 표에 있는 도시는 표를 타서 멀쩡했고 좌표 입력만 틀려서 여태 안 드러났다. 같은 서울을 이름과 좌표로 조회했을 때 일 누적이 9.8mm vs 29mm로 갈리며 발견. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* asos-station.test.ts
|
||||
*
|
||||
* ASOS(종관기상관측) 지점 매칭과 rn 인코딩 정규화.
|
||||
*
|
||||
* 지점 목록은 지점정보 API가 없어서(NO_OPENAPI_SERVICE_ERROR) 일자료 엔드포인트로
|
||||
* stnIds 90~300을 전수조사해 뽑았다 — 97개.
|
||||
*
|
||||
* 좌표 기반 최근접 지점 선택은 만들다 폐기했다: 지점명을 지오코딩했더니 홍성·보령·밀양이
|
||||
* 북한 동명 지역으로, 남원이 제주도로 나왔다. 좌표를 검증하려고 ASOS 관측기온과
|
||||
* Open-Meteo 기온을 대조했지만 좌표가 정확한 서울조차 Δ3.1°C라(도시열섬·모델편차)
|
||||
* 판별자가 못 됐다. 그래서 이름으로만 맞히고, 못 맞히면 Open-Meteo로 명시적으로 물러난다.
|
||||
*/
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { findAsosStation, normalizeAsosHour } from '../src/tools/weather';
|
||||
|
||||
describe('findAsosStation — 지점명 매칭', () => {
|
||||
test('정확히 일치하는 지점', () => {
|
||||
assert.deepEqual(findAsosStation('서울'), { id: 108, name: '서울' });
|
||||
assert.deepEqual(findAsosStation('구미'), { id: 279, name: '구미' });
|
||||
});
|
||||
|
||||
test('행정 접미사를 벗겨서 맞힌다', () => {
|
||||
assert.deepEqual(findAsosStation('부산광역시'), { id: 159, name: '부산' });
|
||||
// 반대 방향: 입력은 맨이름인데 지점명에 "시"가 붙어 있는 경우
|
||||
assert.deepEqual(findAsosStation('김해'), { id: 253, name: '김해시' });
|
||||
});
|
||||
|
||||
test('방위 접두사가 붙은 지점명도 맨이름으로 찾는다', () => {
|
||||
// 지점명은 "북창원"인데 사용자는 "창원"이라고 쓴다.
|
||||
assert.deepEqual(findAsosStation('창원'), { id: 155, name: '창원' });
|
||||
assert.deepEqual(findAsosStation('춘천'), { id: 101, name: '춘천' });
|
||||
});
|
||||
|
||||
test('정확 일치가 접두사 제거보다 우선한다', () => {
|
||||
// "부산"은 159(부산)과 296(북부산) 둘 다에 걸릴 수 있다 — 정확 일치가 이겨야 한다.
|
||||
assert.deepEqual(findAsosStation('부산'), { id: 159, name: '부산' });
|
||||
});
|
||||
|
||||
test('ASOS 지점이 없는 입력은 null (호출부가 Open-Meteo로 물러남)', () => {
|
||||
assert.equal(findAsosStation('우리집'), null);
|
||||
assert.equal(findAsosStation('37.5665,126.978'), null);
|
||||
assert.equal(findAsosStation(''), null);
|
||||
assert.equal(findAsosStation(' '), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeAsosHour — rn 인코딩', () => {
|
||||
test('""는 무강수 (결측 아님)', () => {
|
||||
assert.deepEqual(normalizeAsosHour('2026-08-15 03:00', ''), { time: '2026-08-15T03:00', mm: 0, wet: false });
|
||||
});
|
||||
|
||||
test('"0.0"은 미량 강수 — 0mm지만 비는 온 시각', () => {
|
||||
// 이걸 무강수로 뭉개면 이슬비로 이어지던 비가 소강으로 잡혀 사상이 끊긴다.
|
||||
assert.deepEqual(normalizeAsosHour('2026-08-15 06:00', '0.0'), { time: '2026-08-15T06:00', mm: 0, wet: true });
|
||||
});
|
||||
|
||||
test('실제 강수량', () => {
|
||||
assert.deepEqual(normalizeAsosHour('2026-08-15 16:00', '1.0'), { time: '2026-08-15T16:00', mm: 1, wet: true });
|
||||
});
|
||||
|
||||
test('null/undefined는 무강수로 처리', () => {
|
||||
assert.equal(normalizeAsosHour('2026-08-15 01:00', null).wet, false);
|
||||
assert.equal(normalizeAsosHour('2026-08-15 01:00', undefined).wet, false);
|
||||
});
|
||||
|
||||
test('tm의 공백을 T로 바꿔 사상 판정에서 정렬 가능하게 만든다', () => {
|
||||
// 초단기실황 쪽 시각(2026-08-16T02:00)과 한 배열에 섞여 시간순 정렬되어야 한다.
|
||||
assert.equal(normalizeAsosHour('2026-08-15 23:00', '0.5').time, '2026-08-15T23:00');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user